DEV Community

Cover image for How I Built an AI Resume Optimizer with FastAPI + Claude API (and got 100 users in 4 months)
YASH SARJEKAR
YASH SARJEKAR

Posted on

How I Built an AI Resume Optimizer with FastAPI + Claude API (and got 100 users in 4 months)

Six months ago my cousin was applying to 50+ jobs and getting no callbacks. His resume looked fine. Good experience, right format. But he was being filtered out before any human ever read it.

That's when I went down the ATS rabbit hole.

Applicant Tracking Systems filter 75% of resumes before a recruiter sees them — not because the candidate is unqualified, but because the resume doesn't match the keywords the system is scanning for. Most people have no idea this is happening.

So I built a tool to fix it: Resume Builder AI — an AI-powered resume optimizer that scores your resume against a job description and rewrites it to pass ATS filters. Here's exactly how I built it.

- The Architecture

Frontend:  Next.js 15 (App Router) + TypeScript + Tailwind
Backend:   FastAPI + SQLAlchemy + PostgreSQL
AI:        Claude API (claude-sonnet-4-6)
Deploy:    Railway (backend) + Vercel (frontend)
Auth:      JWT + bcrypt
Payments:  Razorpay
Enter fullscreen mode Exit fullscreen mode

The core flow is a two-stage AI pipeline:

Analyze — Claude reads the resume + job description and returns structured JSON with an ATS score, missing keywords, and specific suggestions
Optimize — Claude rewrites the resume incorporating those changes, but only after the user reviews and approves the proposed changes
Stage 2 was a late addition. My first version applied all AI changes automatically. Users hated it.

Stage 2 was a late addition. My first version applied all AI changes automatically. Users hated it.

- The Claude API Integration
The trickiest part was getting consistent structured output from Claude for the analysis stage. Here's the core pattern I landed on:

from anthropic import Anthropic

client = Anthropic()

ANALYZE_SYSTEM = """
You are an ATS expert. Analyze the resume against the job description.
Return ONLY valid JSON with this exact structure:
{
  "ats_score": <0-100>,
  "missing_keywords": [...],
  "weak_bullets": [...],
  "suggestions": [{"section": "...", "issue": "...", "fix": "..."}]
}
"""

def analyze_resume(resume_text: str, job_description: str) -> dict:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2000,
        system=ANALYZE_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"RESUME:\n{resume_text}\n\nJOB DESCRIPTION:\n{job_description}"
        }]
    )

    # Claude sometimes wraps JSON in markdown fences — strip them
    content = response.content[0].text.strip()
    if content.startswith("```

"):
        content = content.split("

```")[1]
        if content.startswith("json"):
            content = content[4:]

    return json.loads(content)
Enter fullscreen mode Exit fullscreen mode

The key insight: always strip markdown fences even when you explicitly tell the model not to use them. Claude is very good, but it occasionally wraps JSON in fences anyway. Handle it defensively.

- The Two-Stage Design Decision
My V1 was a single-shot rewrite: paste resume + job description → get optimized resume.

Early users didn't come back.

When I asked why, the answer was consistent: "I didn't know what changed. I couldn't trust it."

So I built a preview step. The AI shows what it wants to change — specific bullet rewrites, keywords to add, sections to restructure — and the user approves or rejects each change before anything is applied.

Retention improved significantly after this change.

The lesson: users need to feel in control of AI output, especially for something as personal as a resume. Transparency > convenience.

You can see the current implementation at resumebuilder.pulsestack.in/builder .

- PDF Parsing Was Harder Than Expected
Getting clean text out of uploaded PDFs across different resume formats was the most painful part of the project.

import pdfplumber

def extract_text_from_pdf(file_bytes: bytes) -> str:
    with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
        text = ""
        for page in pdf.pages:
            # extract_text() alone misses multi-column layouts
            # Use bounding boxes for better accuracy
            words = page.extract_words(
                x_tolerance=3,
                y_tolerance=3,
                keep_blank_chars=False
            )
            # Sort by vertical position then horizontal
            words.sort(key=lambda w: (round(w['top'] / 5) * 5, w['x0']))
            text += " ".join(w['text'] for w in words) + "\n"

    return text.strip()
Enter fullscreen mode Exit fullscreen mode

Multi-column resume layouts were the hardest case. Standard

extract_text()
Enter fullscreen mode Exit fullscreen mode

reads columns left-to-right across the page rather than column by column, completely mangling the structure. The bounding-box approach above handles most layouts correctly.

- ATS Scoring Logic
The score is calculated across 5 dimensions:

def calculate_ats_score(resume: dict, job_desc: str) -> int:
    scores = {
        "keyword_match":    _keyword_overlap(resume, job_desc) * 0.35,
        "format_clean":     _format_score(resume)              * 0.20,
        "bullet_strength":  _bullet_action_verbs(resume)       * 0.20,
        "section_complete": _required_sections(resume)         * 0.15,
        "length_fit":       _length_score(resume)              * 0.10,
    }
    return round(sum(scores.values()))
Enter fullscreen mode Exit fullscreen mode

Keyword match is weighted heaviest (35%) because it's what most ATS systems primarily filter on. Format cleanliness matters more than most people think — fancy tables, headers/footers, and text boxes confuse parsers.

What I'd Do Differently

1. Talk to users before building features.
I spent 3 weeks building a LinkedIn optimizer nobody asked for. The ATS score feature — which I almost didn't build — is the thing people actually share.

2. Launch earlier.
I waited until the product felt "ready." Wasted 6 weeks. Nothing teaches you what to build faster than real users trying to use it.

3. Invest in SEO from day 1.
I'm 4 months in and still fighting for Google indexing on a new domain. Starting a blog and building backlinks from week 1 would have saved months of catching up.

Current State
The tool is live at resumebuilder.pulsestack.in with:

  • ATS scoring against any job description
  • AI resume optimization with preview + approve flow
  • 6 PDF templates including a LaTeX-compiled formal CV
  • Cover letter generator
  • LinkedIn optimizer
  • Free tier, no credit card required
  • 100+ users, growing purely through word of mouth so far.

If you're curious about any part of the technical implementation — the Claude API prompting strategy, the FastAPI architecture, the PDF generation pipeline — happy to go deeper in the comments.

And if you're currently job hunting, try running your resume through the ATS checker — the score alone is usually eye-opening.

Built with FastAPI, Next.js, Claude API, PostgreSQL, and Railway. Source not public yet but considering it.

Top comments (0)