Back to blog
6 min read280 views

How to Build an ATS-Optimized AI Resume Builder with Next.js 14 & Gemini API

Engineering guide to parsing unstructured career history, calculating ATS keyword relevance scores, and generating pixel-perfect PDF resumes using Next.js 14 and Gemini 1.5.

Next.jsAI EngineeringGemini APITypeScriptCareer ToolsWeb Development

Why Most Online Resume Builders Fail Applicant Tracking Systems (ATS)

Applicant Tracking Systems (such as Greenhouse, Workday, and Lever) parse incoming resumes by converting raw document streams into structured JSON fields: Work Experience, Education, Skills, and Quantifiable Impact. Most graphical resume generators use complex multi-column CSS tables, floating text boxes, or non-standard fonts that break ATS document parsers completely.

In **Day 03 of the 30-Days AI Projects Series (Smart Resume Builder & ATS Optimizer)**, I engineered an ATS-first resume generation engine. Here is the full technical breakdown.

---

1. ATS Keyword Density & Relevance Scoring Algorithm

To provide applicants with real-time feedback, the application parses the target Job Description against the resume text using Token Frequency-Inverse Document Frequency (TF-IDF) extraction and cosine keyword matching:

```typescript // lib/ats-scorer.ts export interface ATSAnalysisResult { score: number; // 0 to 100 matchedKeywords: string[]; missingKeywords: string[]; formattingFlags: string[]; bulletPointImprovements: { original: string; suggested: string; reason: string }[]; }

export function calculateATSScore(resumeText: string, jobDescriptionText: string): ATSAnalysisResult { const cleanText = (t: string) => t.toLowerCase().replace(/[^a-z0-9\s]/g, ' '); const resumeWords = new Set(cleanText(resumeText).split(/\s+/).filter((w) => w.length > 3)); const jobKeywords = cleanText(jobDescriptionText) .split(/\s+/) .filter((w) => w.length > 3 && !COMMON_STOPWORDS.has(w)); const uniqueJobKeywords = Array.from(new Set(jobKeywords)); const matched = uniqueJobKeywords.filter((k) => resumeWords.has(k)); const missing = uniqueJobKeywords.filter((k) => !resumeWords.has(k)); const keywordCoverage = (matched.length / Math.max(1, uniqueJobKeywords.length)) * 100; // Check for quantifiable impact (numbers, percentages, dollar signs) const hasMetrics = /\b(\d+%|\$\d+|\d+x|reduced by|increased by)\b/i.test(resumeText); let score = Math.round(keywordCoverage * 0.7 + (hasMetrics ? 30 : 10)); score = Math.min(98, Math.max(20, score)); return { score, matchedKeywords: matched.slice(0, 15), missingKeywords: missing.slice(0, 15), formattingFlags: hasMetrics ? [] : ['Lacks quantifiable impact metrics (e.g. %, $ amounts, latency reduction)'], bulletPointImprovements: [] }; } ```

---

2. Gemini 1.5 Structured Bullet Point Rewriting

Instead of generic AI text generation, we instruct Gemini 1.5 Flash to rewrite bullet points strictly using Google's XYZ formula (*"Accomplished [X], as measured by [Y], by doing [Z]"*):

```typescript import { GoogleGenerativeAI } from '@google/generative-ai';

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

export async function optimizeResumeBulletPoints(bullets: string[], targetRole: string) { const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash', generationConfig: { responseMimeType: 'application/json' } });

const prompt = You are an expert technical resume reviewer. Rewrite the following resume bullet points for a ${targetRole} role. Apply the Google XYZ formula: 'Accomplished [X] as measured by [Y], by doing [Z]'. Input bullets: ${JSON.stringify(bullets)} Output Schema: [ { "original": "Managed database servers", "suggested": "Optimized PostgreSQL query execution plans, reducing p99 API latency by 45% across 200k daily requests", "reason": "Added specific metrics, technology keywords, and measurable business impact." } ] ;

const response = await model.generateContent(prompt); return JSON.parse(response.response.text()); } ```

---

3. Key Takeaways for Full-Stack AI Builders

* **Structured Outputs are Essential**: Constraining generation to application/json prevents parsing failures on client web apps. * **Design for Single-Column Flow**: ATS parsers read from top-to-bottom; avoid complex nested flexbox tables in export templates. * **Client-Side PDF Generation**: Using browser print stylesheets or client-side Canvas APIs removes expensive serverless headless Chrome dependencies.

---

*Test the live Smart Resume Builder on [aiwithab.site/mini-projects](https://www.aiwithab.site/mini-projects).*

Rate this article

5.0 / 5.0 (21 votes)

Was this helpful?