Skip to main content

Overview

After generating an interview report, you can create a tailored resume PDF that:
  • Highlights relevant experience for the specific job
  • Uses ATS-friendly formatting
  • Presents professional, human-like content
  • Optimizes for 1-2 page length

How It Works

The resume PDF generation process uses AI to rewrite your resume content specifically for the job you’re applying to, then converts the HTML output to a professional PDF using Puppeteer.
1

Generate Interview Report First

You must create an interview report before generating a resume PDF. The report ID is required:
const { interviewReport } = await generateInterviewReport({
  jobDescription: "Senior Backend Engineer...",
  resumeFile: file,
  selfDescription: "8 years of experience..."
})

const reportId = interviewReport._id
2

Call the Resume PDF Endpoint

Send a POST request to generate the PDF:
POST /api/interview/resume/pdf/:interviewReportId
The endpoint is defined in interview.routes.js:38:
interviewRouter.post(
  "/resume/pdf/:interviewReportId", 
  authMiddleware.authUser, 
  interviewController.generateResumePdfController
)
3

AI Generates HTML Resume

The system retrieves your interview report data and sends it to Google Gemini AI:
  • Original resume content
  • Job description
  • Self-description
The AI generates tailored HTML optimized for the specific role. Implementation in ai.service.js:98-124.
4

HTML to PDF Conversion

Puppeteer converts the HTML to a professionally formatted PDF:
const pdfBuffer = await page.pdf({
  format: "A4",
  margin: {
    top: "20mm",
    bottom: "20mm",
    left: "15mm",
    right: "15mm"
  }
})
From ai.service.js:84-91.
5

Download PDF

The server sends the PDF as a downloadable file:
Content-Type: application/pdf
Content-Disposition: attachment; filename=resume_60f7b3b3b3b3b3b3b3b3b3b3.pdf
Implementation in interview.controller.js:187-192.

API Usage

Endpoint

POST /api/interview/resume/pdf/:interviewReportId

Parameters

ParameterTypeLocationDescription
interviewReportIdstringURL pathMongoDB ObjectId of the interview report

Authentication

Requires active session cookie:
credentials: "include"

Response

Returns binary PDF data with appropriate headers:
  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename=resume_{reportId}.pdf

JavaScript Example

import { generateResumePdf } from './features/interview/services/interview.api'

async function downloadResume(interviewReportId) {
  try {
    // Get PDF blob
    const pdfBlob = await generateResumePdf({ interviewReportId })
    
    // Create download link
    const url = window.URL.createObjectURL(pdfBlob)
    const link = document.createElement('a')
    link.href = url
    link.download = `resume_${interviewReportId}.pdf`
    
    // Trigger download
    document.body.appendChild(link)
    link.click()
    
    // Cleanup
    document.body.removeChild(link)
    window.URL.revokeObjectURL(url)
    
  } catch (error) {
    if (error.response?.status === 404) {
      alert('Interview report not found')
    } else {
      alert('Failed to generate resume PDF')
    }
  }
}
Based on interview.api.js:60-66.

Frontend Service Implementation

The complete service method from interview.api.js:60-66:
export const generateResumePdf = async ({ interviewReportId }) => {
    const response = await api.post(
      `/api/interview/resume/pdf/${interviewReportId}`, 
      null, 
      {
        responseType: "blob"
      }
    )
    
    return response.data
}
The responseType: "blob" option is critical for handling binary PDF data correctly.

Backend Implementation

Controller Flow

From interview.controller.js:172-193:
async function generateResumePdfController(req, res) {
    const { interviewReportId } = req.params
    
    // 1. Fetch interview report
    const interviewReport = await interviewReportModel.findById(interviewReportId)
    
    if (!interviewReport) {
        return res.status(404).json({
            message: "Interview report not found."
        })
    }
    
    // 2. Extract required data
    const { resume, jobDescription, selfDescription } = interviewReport
    
    // 3. Generate PDF via AI service
    const pdfBuffer = await generateResumePdf({ 
      resume, 
      jobDescription, 
      selfDescription 
    })
    
    // 4. Set response headers
    res.set({
        "Content-Type": "application/pdf",
        "Content-Disposition": `attachment; filename=resume_${interviewReportId}.pdf`
    })
    
    // 5. Send PDF
    res.send(pdfBuffer)
}

AI Service

The AI generation process from ai.service.js:98-132:
async function generateResumePdf({ resume, selfDescription, jobDescription }) {
    
    // Define schema for AI response
    const resumePdfSchema = z.object({
        html: z.string().describe(
          "The HTML content of the resume which can be converted to PDF"
        )
    })
    
    // Create prompt
    const prompt = `Generate resume for a candidate with the following details:
                    Resume: ${resume}
                    Self Description: ${selfDescription}
                    Job Description: ${jobDescription}

                    The resume should be tailored for the given job description 
                    and should highlight the candidate's strengths and relevant experience.
                    The HTML content should be well-formatted and structured, 
                    making it easy to read and visually appealing.
                    The content should not sound like it's generated by AI.
                    The resume should ideally be 1-2 pages long when converted to PDF.
                    Focus on quality rather than quantity.`
    
    // Call AI
    const response = await ai.models.generateContent({
        model: "gemini-3-flash-preview",
        contents: prompt,
        config: {
            responseMimeType: "application/json",
            responseSchema: zodToJsonSchema(resumePdfSchema)
        }
    })
    
    // Parse response
    const jsonContent = JSON.parse(response.text)
    
    // Convert HTML to PDF
    const pdfBuffer = await generatePdfFromHtml(jsonContent.html)
    
    return pdfBuffer
}

PDF Configuration

Puppeteer is configured for professional output:
const pdfBuffer = await page.pdf({
    format: "A4",      // Standard paper size
    margin: {
        top: "20mm",    // Professional margins
        bottom: "20mm",
        left: "15mm",
        right: "15mm"
    }
})
From ai.service.js:84-91.
The A4 format and consistent margins ensure the resume looks professional when printed or viewed digitally.

AI Resume Generation Guidelines

The AI follows these principles when creating your resume:

Content Quality

  • Tailored to Job: Emphasizes experience and skills matching the job description
  • Human-Like Writing: Avoids AI-sounding language patterns
  • Concise: Targets 1-2 pages with high-quality content over quantity
  • Relevant Details: Highlights experiences that align with the role

ATS Optimization

  • Parsable Structure: Uses semantic HTML that ATS systems can read
  • Keyword Rich: Includes relevant keywords from the job description
  • Standard Sections: Education, Experience, Skills clearly delineated
  • Clean Formatting: No complex tables or graphics that confuse parsers

Visual Design

  • Professional Styling: Clean, modern design with appropriate spacing
  • Selective Color: Minimal color highlights for visual interest
  • Readable Fonts: Standard fonts that render consistently
  • Clear Hierarchy: Section headers, job titles, and dates are distinct
See the complete prompt in ai.service.js:104-115.

Error Handling

Interview Report Not Found (404)

{
  "message": "Interview report not found."
}
Causes:
  • Invalid interviewReportId
  • Report was deleted
  • Report belongs to a different user
Solution: Verify the report ID and ensure it exists.

Authentication Required (401)

No specific error body - the auth middleware redirects. Cause: No valid session cookie Solution: Ensure the user is logged in and include credentials:
credentials: "include"

AI Service Failure

If the AI service fails, an error is thrown and handled by Express error middleware. Common causes:
  • Google Gemini API rate limits
  • API key issues
  • Network connectivity problems

Complete Example

Full Workflow: Report to PDF

import { 
  generateInterviewReport, 
  generateResumePdf 
} from './features/interview/services/interview.api'

async function completeWorkflow() {
  // Step 1: Generate interview report
  const reportResult = await generateInterviewReport({
    jobDescription: `
      Senior Software Engineer - Backend
      
      Requirements:
      - 5+ years Node.js experience
      - Microservices architecture
      - PostgreSQL and MongoDB
      - AWS deployment
    `,
    resumeFile: document.getElementById('resume').files[0],
    selfDescription: `
      Senior backend engineer with 7 years building scalable APIs.
      Led migration from monolith to microservices at previous company.
      Deep experience with Node.js, Express, and database optimization.
    `,
    title: "Senior Software Engineer - Backend"
  })
  
  const reportId = reportResult.interviewReport._id
  console.log('Interview report created:', reportId)
  console.log('Match score:', reportResult.interviewReport.matchScore)
  
  // Step 2: Generate tailored resume PDF
  const pdfBlob = await generateResumePdf({ 
    interviewReportId: reportId 
  })
  
  // Step 3: Download PDF
  const url = window.URL.createObjectURL(pdfBlob)
  const link = document.createElement('a')
  link.href = url
  link.download = `resume_backend_engineer.pdf`
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  window.URL.revokeObjectURL(url)
  
  console.log('Resume PDF downloaded successfully')
}

React Component Example

import { useState } from 'react'
import { generateResumePdf } from '../services/interview.api'

function ResumePdfButton({ interviewReportId }) {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  
  const handleDownload = async () => {
    setLoading(true)
    setError(null)
    
    try {
      const pdfBlob = await generateResumePdf({ interviewReportId })
      
      const url = window.URL.createObjectURL(pdfBlob)
      const link = document.createElement('a')
      link.href = url
      link.download = `tailored_resume_${interviewReportId}.pdf`
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      window.URL.revokeObjectURL(url)
      
    } catch (err) {
      setError(err.response?.data?.message || 'Failed to generate PDF')
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <div>
      <button 
        onClick={handleDownload} 
        disabled={loading}
      >
        {loading ? 'Generating PDF...' : 'Download Tailored Resume'}
      </button>
      {error && <p className="error">{error}</p>}
    </div>
  )
}

Best Practices

Review Before Sending

Always review the generated PDF before submitting to employers. Verify accuracy and formatting.

Generate Per Application

Create a new tailored resume for each job application rather than using a generic version.

Update Source Resume

Keep your original resume current. Better source data produces better tailored resumes.

Check File Size

PDFs are typically 50-200KB. Larger files may indicate issues with embedded content.

Tips for Better Resumes

Provide detailed self-descriptions: The more context you give about your work, the better the AI can highlight relevant achievements.
Include metrics in your original resume: Numbers and quantifiable achievements translate well to tailored versions (e.g., “Reduced API latency by 40%”).
Use the full job description: Include requirements, responsibilities, and company info for better keyword matching.
Always fact-check: The AI tailors your resume but should never fabricate experience. Verify all content is truthful.

Troubleshooting

This can occur if:
  • The AI generated invalid HTML
  • Puppeteer failed to load resources
Solution: Try generating again. The AI model occasionally produces non-standard HTML.
The AI significantly rewrites your resume to match the job description.Solution: Ensure your original resume and self-description accurately represent your experience. The AI can only work with the information provided.
Browser security settings may block automatic downloads.Solution:
  • Check browser console for errors
  • Ensure pop-ups aren’t blocked
  • Try a different browser
The AI targets 1-2 pages but may vary based on your experience.Solution: Adjust your self-description length. More detailed input typically produces longer resumes.

Next Steps

Interview Reports Guide

Learn how to create comprehensive interview reports

API Reference

View complete API documentation for PDF generation

Build docs developers (and LLMs) love