Build an ATS Resume Optimizer API

Build a REST API that rewrites your resume for ATS using AI.

Introduction

30 Second Summary

Every time you apply for a job online, your resume passes through software that scans it for the right keywords before a human ever reads it. If your wording doesn't match what the system expects, your application gets buried.

In this project, you will build a local REST API that accepts your resume and a target job description, uses AI to rewrite your bullet points with the right keywords, and returns an optimized .docx file ready to submit.

What You'll Build

You'll open your browser, upload your actual resume, paste a real job description, and watch your API hand back a tailored version complete with an ATS score and rewritten bullet points.

By the end of this project, you'll have:

  • A working resume optimization API that accepts a .docx file and a job description, then returns rewritten bullet points with an ATS compatibility score and a list of missing keywords.
  • A document download endpoint that rebuilds your resume with the optimized content while preserving your original formatting, fonts, and layout.
  • A template suggestions feature that recommends ATS-friendly resume structures based on your target role.
  • Secret Mission: Add a comparison endpoint that scores your original vs. optimized resume side-by-side.

Are there any prerequisites?

None. This guide walks you through installing everything you need, including Python, VS Code, and a free Google Gemini API key.

Before We Start

Before we dive into any code, let's lock in what you're about to build and why it matters to you.

Build the Resume Reader

Your environment is set up and ready to go. Now it is time to write the first piece of real functionality.

Before AI can optimize anything, your API needs to extract text from a .docx file while remembering the document structure. Which paragraphs are headings? Which are bullet points? That structural awareness is what lets the AI make smart, targeted rewrites later.

In this step, get ready to:
  • Build a resume parser that extracts structured sections from a .docx file.
  • Create a FastAPI app with a /parse endpoint to upload and parse resumes.
  • Add an /analyze endpoint skeleton that pairs resume text with a job description.
Create the resume parser module

The parser is the foundation of the entire API. It opens a .docx file, walks through every paragraph, and groups the content under section headings.

  • Create a new file called resume_parser.py in your resume-optimizer folder and add the following code:
from io import BytesIO
from docx import Document


def parse_resume(file_stream: BytesIO) -> list[dict]:
    # Open the .docx file from a byte stream
    document = Document(file_stream)
    sections = []
    current_section = {"heading": "Header", "bullets": []}

    for paragraph in document.paragraphs:
        text = paragraph.text.strip()
        if not text:
            continue

        # Check the paragraph's Word style name
        style_name = paragraph.style.name if paragraph.style else ""

        # If it's a heading or title, start a new section
        if "Heading" in style_name or style_name == "Title":
            if current_section["bullets"]:
                sections.append(current_section)
            current_section = {"heading": text, "bullets": []}
        else:
            current_section["bullets"].append(text)

    # Don't forget the last section
    if current_section["bullets"]:
        sections.append(current_section)

    return sections

What does this code do?

The parse_resume function accepts a file stream (bytes in memory) and uses python-docx to open it as a Word document.

It loops through every paragraph and checks its Word style name. If the style contains "Heading" or equals "Title", the function treats it as a new section header. Everything else gets collected as bullet content under the current heading.

The result is a list of dictionaries, each with a heading string and a bullets array of text from that section.

  • Save resume_parser.py.
Create the FastAPI app with the /parse endpoint

Now you need the API layer that accepts file uploads and calls your parser. FastAPI provides built-in file upload handling and generates interactive docs automatically.

  • Create a new file called main.py in the same resume-optimizer folder and add the following code:
import os
from io import BytesIO
from fastapi import FastAPI, UploadFile, File, Form, Query
from fastapi.responses import StreamingResponse
from dotenv import load_dotenv

from resume_parser import parse_resume

# Load environment variables from .env
load_dotenv()

# Create the FastAPI application
app = FastAPI(title="ATS Resume Optimizer")


@app.post("/parse")
async def parse(file: UploadFile = File(...)):
    # Read the uploaded file into memory
    contents = await file.read()
    stream = BytesIO(contents)
    # Parse the resume and return structured sections
    sections = parse_resume(stream)
    return {"sections": sections}

What does this code do?

This creates a FastAPI application with a single POST endpoint at /parse. When someone uploads a file, UploadFile reads the bytes into memory, wraps them in a BytesIO stream, and passes that to your parser.

The extra imports (StreamingResponse, Form, Query) are used by endpoints you add in later steps.

  • Save main.py.
  • Start the development server by running this command in your terminal:
fastapi dev

What does this command do?

The fastapi dev command auto-detects the app object in your main.py and starts a development server at http://127.0.0.1:8000 with auto-reload enabled. Any time you save a file, the server restarts automatically.

This command takes over your terminal. You will see log output including the server URL. Keep it running for the rest of this step.

Before you test, think about what you expect. If you upload a Word document with heading-styled sections, what shape should the JSON response take?

  • Open your browser and go to http://127.0.0.1:8000/docs.
  • Click the POST /parse endpoint to expand it.
  • Click Try it out.
  • Click Choose File and select any .docx file from your computer (a resume, a report, or any Word document with headings).
  • Click Execute.

You should see a JSON response with a sections array. Each item contains a heading string and a bullets array of text from that section.

No .docx file handy?

Open Google Docs or Microsoft Word, type a few lines with one heading (format it as Heading 1), add a few bullet points below it, then download as .docx. That is enough to test with.

Server not starting?

If you see ModuleNotFoundError: No module named 'resume_parser', make sure resume_parser.py is saved in the same folder as main.py and that you ran fastapi dev from the resume-optimizer folder.

If the upload returns empty sections, your test document may not use Word heading styles. Make sure at least one paragraph is formatted as Heading 1 or Title in Word.

Help me debug my FastAPI server

✔️ Awesome, I've got everything!

Great. Make sure resume_parser.py is saved. You will build on it throughout the project.

ⓧ I'd like to double check the full code

Here is the complete resume_parser.py file:

from io import BytesIO
from docx import Document


def parse_resume(file_stream: BytesIO) -> list[dict]:
    document = Document(file_stream)
    sections = []
    current_section = {"heading": "Header", "bullets": []}

    for paragraph in document.paragraphs:
        text = paragraph.text.strip()
        if not text:
            continue

        style_name = paragraph.style.name if paragraph.style else ""

        if "Heading" in style_name or style_name == "Title":
            if current_section["bullets"]:
                sections.append(current_section)
            current_section = {"heading": text, "bullets": []}
        else:
            current_section["bullets"].append(text)

    if current_section["bullets"]:
        sections.append(current_section)

    return sections

And here is main.py so far (just the imports and /parse endpoint):

import os
from io import BytesIO
from fastapi import FastAPI, UploadFile, File, Form, Query
from fastapi.responses import StreamingResponse
from dotenv import load_dotenv

from resume_parser import parse_resume

load_dotenv()

app = FastAPI(title="ATS Resume Optimizer")


@app.post("/parse")
async def parse(file: UploadFile = File(...)):
    contents = await file.read()
    stream = BytesIO(contents)
    sections = parse_resume(stream)
    return {"sections": sections}
Add the analyze endpoint

The /parse endpoint returns raw sections, but optimization needs to compare resume text against a job description. The /analyze endpoint accepts both inputs and pairs them together.

  • In main.py, add the following code below the /parse endpoint:
@app.post("/analyze")
async def analyze(file: UploadFile = File(...), job_description: str = Form(...)):
    # Read and parse the uploaded resume
    contents = await file.read()
    stream = BytesIO(contents)
    sections = parse_resume(stream)
    # Combine sections into a single text block
    resume_text = "\n".join(
        f"## {s['heading']}\n" + "\n".join(s["bullets"])
        for s in sections
    )
    return {"resume_text": resume_text, "job_description": job_description}

What does this code do?

This endpoint accepts two inputs: a file upload and a job_description text field. It parses the resume into sections, then flattens them into a single text string with markdown-style headings.

For now, it just returns both pieces of data side by side. In the next step, you will send this paired text to Gemini AI for intelligent analysis.

  • Save main.py. The dev server auto-reloads.
  • Switch back to http://127.0.0.1:8000/docs in your browser and refresh the page.
  • Click the POST /analyze endpoint to expand it.
  • Click Try it out.
  • Upload the same .docx file.
  • In the job_description field, type a short job description like Looking for a backend engineer with Python and API experience.
  • Click Execute.

You should see a JSON response with resume_text (your resume flattened into one string with ## headings) and job_description echoed back.

Seeing a 422 Unprocessable Entity?

A 422 error means FastAPI did not receive the expected fields. Make sure you filled in both the file upload and the job_description text field before clicking Execute. Both are required.

Help me fix my 422 error

✔️ Awesome, I've got everything!

Great. Double check you have saved both files in your resume-optimizer folder.

ⓧ I'd like to double check the full code

Here is the complete main.py with both endpoints:

import os
from io import BytesIO
from fastapi import FastAPI, UploadFile, File, Form, Query
from fastapi.responses import StreamingResponse
from dotenv import load_dotenv

from resume_parser import parse_resume

load_dotenv()

app = FastAPI(title="ATS Resume Optimizer")


@app.post("/parse")
async def parse(file: UploadFile = File(...)):
    contents = await file.read()
    stream = BytesIO(contents)
    sections = parse_resume(stream)
    return {"sections": sections}

Followed by the /analyze endpoint:

@app.post("/analyze")
async def analyze(file: UploadFile = File(...), job_description: str = Form(...)):
    contents = await file.read()
    stream = BytesIO(contents)
    sections = parse_resume(stream)
    resume_text = "\n".join(
        f"## {s['heading']}\n" + "\n".join(s["bullets"])
        for s in sections
    )
    return {"resume_text": resume_text, "job_description": job_description}

Your API can now accept a resume and a job description, extract the structured content, and pair them together. Next up, you will wire in Gemini AI to compare these two inputs and produce an optimized version of your resume.

Add AI-Powered Optimization

Your API can parse resumes into structured text, but the /analyze endpoint just echoes it back without any intelligence. That's about to change.

In this step, you'll create a dedicated AI module that sends your resume text and a job description to Google Gemini. Gemini compares them, identifies missing keywords, and rewrites bullet points to improve ATS compatibility.

In this step, get ready to:
  • Create a Gemini client module with a structured optimization prompt.
  • Wire a new /optimize endpoint that returns ATS scores, missing keywords, and rewritten bullets.
  • Prepare a template suggestions function for a later step.
Create the Gemini client module

The AI logic lives in its own module so your API routes stay clean. This module initializes the Gemini client, defines the system instruction that tells the model how to behave, and exposes a function your endpoints can call.

  • In the VS Code Explorer sidebar, right-click inside your resume-optimizer folder and select New File.
  • Name the file gemini_client.py.
  • Add the imports, client initialization, and system instruction by pasting this code:
import os
import json
from google import genai
from google.genai import types

# Initialize the Gemini client using your API key from .env
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

# System instruction that tells Gemini how to analyze and optimize resumes
OPTIMIZE_SYSTEM_INSTRUCTION = """You are an expert ATS (Applicant Tracking System) resume optimizer.
Given a resume and a job description, you must:
1. Score the current resume for ATS compatibility (0-100)
2. Identify keywords from the JD that are missing in the resume
3. Rewrite the resume bullet points to naturally incorporate missing keywords while preserving the candidate's actual experience. Do not fabricate experience.

Respond in JSON with this exact structure:
{
  "ats_score": <number>,
  "missing_keywords": [<list of strings>],
  "optimized_bullets": [{"original": "<original text>", "optimized": "<rewritten text>"}]
}"""

What does this code do?

The genai.Client connects to Google's Gemini API using the key you stored in .env. The load_dotenv() call in main.py already loads that variable into the environment.

The system instruction is a detailed prompt that constrains Gemini's behaviour. It forces the model to return a specific JSON structure with an ATS score, missing keywords, and rewritten bullets. This ensures your API always gets predictable, parseable output.

  • Now add the optimize_resume function below the system instruction. This function takes resume text and a job description, sends them to Gemini, and returns structured JSON:

def optimize_resume(resume_text: str, job_description: str) -> dict:
    # Combine resume and JD into one prompt for Gemini
    prompt = f"RESUME:\n{resume_text}\n\nJOB DESCRIPTION:\n{job_description}"
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=OPTIMIZE_SYSTEM_INSTRUCTION,
            response_mime_type="application/json",
        ),
    )
    return json.loads(response.text)

What does this function do?

The response_mime_type="application/json" parameter forces Gemini to return valid JSON instead of freeform text. This eliminates parsing issues and means you can call json.loads() on the response with confidence.

The system_instruction is separate from contents. The system instruction sets the model's role and output format. The contents hold the actual data (resume + JD) for this specific request.

  • Save gemini_client.py.
  • Verify the module loads without errors by running this in your terminal:
python -c "from gemini_client import optimize_resume; print('Module loaded')"

You should see Module loaded printed with no errors.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • Check that gemini_client.py is saved in the same resume-optimizer folder where you run the command.
  • If you see an error about GEMINI_API_KEY, make sure your .env file exists and contains your key. The test import reads the environment directly, so run python -c "from dotenv import load_dotenv; load_dotenv(); from gemini_client import optimize_resume; print('OK')" instead.

Still stuck? Help me debug my gemini_client.py import error

Add the template suggestions function

You'll build a template suggestions endpoint in a later step. Adding the function now keeps all Gemini logic in one place and avoids touching this file again later.

  • In gemini_client.py, add the following code below the optimize_resume function:

# System instruction for template/structure recommendations
TEMPLATES_SYSTEM_INSTRUCTION = """You are a resume structure expert. Given a target role, suggest 3 ATS-friendly resume templates.

Respond in JSON with this exact structure:
{
  "templates": [
    {
      "name": "<template name>",
      "sections": [<ordered list of section names>],
      "tips": [<list of formatting tips>],
      "summary_format": "<one-line example summary>"
    }
  ]
}"""


def suggest_templates(role: str) -> list[dict]:
    # Ask Gemini for role-specific resume structure advice
    prompt = f"Suggest 3 ATS-friendly resume templates for the role: {role}"
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=TEMPLATES_SYSTEM_INSTRUCTION,
            response_mime_type="application/json",
        ),
    )
    result = json.loads(response.text)
    return result["templates"]

What does this function do?

The suggest_templates function follows the same pattern as optimize_resume. It has its own system instruction scoped to resume structure recommendations. You'll wire an endpoint to it in Step 5.

  • Save gemini_client.py.
  • Confirm both functions are importable by running:
python -c "from gemini_client import optimize_resume, suggest_templates; print('Both functions ready')"

You should see Both functions ready with no errors.

Seeing a SyntaxError?

  • Check that the triple-quoted string for TEMPLATES_SYSTEM_INSTRUCTION ends with """ on the same line as the closing brace.
  • Make sure there is a blank line between the end of optimize_resume and the start of TEMPLATES_SYSTEM_INSTRUCTION.

Still stuck? Help me fix a SyntaxError in gemini_client.py

✔️ Awesome, I've got everything!

Great. Make sure you've saved gemini_client.py before moving on.

ⓧ I'd like to double check the full code

Your gemini_client.py has three sections you built above. Scroll up and confirm each one is present in this order:

  • Imports, client initialization, and OPTIMIZE_SYSTEM_INSTRUCTION (the first chunk you pasted).
  • The optimize_resume function (second chunk).
  • TEMPLATES_SYSTEM_INSTRUCTION and the suggest_templates function (third chunk).
Wire the /optimize endpoint

The AI module is ready. Now you connect it to your FastAPI app so clients can send a resume and job description and get back the optimization results.

  • In main.py, find this import line near the top:
from resume_parser import parse_resume
  • Replace it with this line that also imports your new Gemini functions:
from resume_parser import parse_resume
from gemini_client import optimize_resume, suggest_templates
  • Now add the /optimize endpoint below the existing /analyze endpoint. Paste this code at the bottom of main.py:

@app.post("/optimize")
async def optimize(file: UploadFile = File(...), job_description: str = Form(...)):
    # Read the uploaded resume file
    contents = await file.read()
    stream = BytesIO(contents)
    # Parse the resume into structured sections
    sections = parse_resume(stream)
    resume_text = "\n".join(
        f"## {s['heading']}\n" + "\n".join(s["bullets"])
        for s in sections
    )
    # Send to Gemini for optimization and return the result
    result = optimize_resume(resume_text, job_description)
    return result

What does this endpoint do?

This endpoint ties together everything you've built. It accepts a .docx file upload and a job description string. It parses the resume using your existing parse_resume function, then passes the text to optimize_resume which calls Gemini and returns structured JSON.

The response the caller receives contains the ATS score, missing keywords, and rewritten bullet points. All from a single API call.

  • Save main.py. Your FastAPI dev server auto-reloads when it detects the change.

Before you test, what do you think will happen when you upload a resume and paste a job description? Will you get back raw text, or structured data with a score?

  • Open http://127.0.0.1:8000/docs in your browser.
  • Click the POST /optimize section to expand it, then click Try it out.
  • Upload a .docx resume file in the file field.
  • In the job_description field, paste a real job listing (or type a short one like Backend engineer with Python, FastAPI, and AWS experience).
  • Click Execute.

You should see a JSON response containing an ats_score number, a missing_keywords array, and an optimized_bullets array with rewritten text that incorporates keywords from the job description.

Seeing a 500 Internal Server Error?

  • Check your terminal for the full error traceback. If it mentions GEMINI_API_KEY, make sure your .env file has the correct key and that load_dotenv() is called before the import of gemini_client in main.py.
  • If you see a json.JSONDecodeError, Gemini may have returned an unexpected format. Try the request again. The response_mime_type parameter should enforce JSON, but occasional retries help.
  • Make sure you uploaded a .docx file, not a .pdf or .txt. The parser only handles Word documents.

Still stuck? Help me debug my /optimize endpoint returning a 500 error

Your API now has real intelligence behind it. Next up, you'll turn that JSON response into a downloadable .docx file the user can submit directly to a job application.

Generate the Optimized Document

Your /optimize endpoint returns AI-rewritten bullet points as JSON. That's helpful for inspecting results, but you can't submit a JSON object to a job application.

In this step, you'll build a document processing module that takes the original .docx file, swaps in the optimized text while preserving all formatting, and returns a downloadable file ready to submit to an employer.

In this step, get ready to:
  • Create a document builder module that preserves .docx formatting while replacing text.
  • Add a download endpoint that runs the full optimization pipeline and returns a .docx file.
  • Test the endpoint by downloading an optimized resume.
Create the document builder module

A .docx file stores text inside Runs. Each Run carries its own formatting (bold, italic, font). To swap text without destroying the layout, you replace the text inside existing Runs rather than deleting and recreating paragraphs.

  • Create a new file called document_builder.py in your resume-optimizer folder.
  • Add the following code to build the optimized document:
from io import BytesIO
from docx import Document


def build_optimized_document(original_stream: BytesIO, optimized_bullets: list[dict]) -> BytesIO:
    # Open the original document to preserve its formatting
    document = Document(original_stream)

    # Map original text to optimized text for quick lookup
    optimization_map = {item["original"]: item["optimized"] for item in optimized_bullets}

    for paragraph in document.paragraphs:
        text = paragraph.text.strip()
        if text in optimization_map:
            new_text = optimization_map[text]
            # Preserve Run formatting by replacing text in the first run
            if paragraph.runs:
                first_run = paragraph.runs[0]
                for run in paragraph.runs[1:]:
                    run.text = ""
                first_run.text = new_text
            else:
                paragraph.text = new_text

    # Save to a new BytesIO stream and return
    output = BytesIO()
    document.save(output)
    output.seek(0)
    return output

What does this code do?

The function opens the original .docx without modifying its style definitions, headers, or layout. It only touches paragraph text.

  • optimization_map creates a dictionary mapping each original bullet to its AI-rewritten version for O(1) lookup.
  • For each paragraph whose text matches an original bullet, the code puts all new text into the first Run and empties the rest. This keeps the first Run's font, size, bold, and italic settings intact.
  • The rebuilt document is saved to a BytesIO stream so it can be returned directly from the API without writing to disk.
  • Save document_builder.py.

Why preserve Runs instead of replacing the whole paragraph?

If you overwrite paragraph.text directly, python-docx deletes all existing Runs and creates a single unstyled one. That strips bold, italic, fonts, and any character-level formatting the user applied.

By writing into the existing first Run and blanking the others, you keep whatever formatting was already there.

✔️ Awesome, I've got everything!

Great. Make sure you've saved document_builder.py before moving on.

ⓧ I'd like to double check the full code

from io import BytesIO
from docx import Document


def build_optimized_document(original_stream: BytesIO, optimized_bullets: list[dict]) -> BytesIO:
    document = Document(original_stream)

    optimization_map = {item["original"]: item["optimized"] for item in optimized_bullets}

    for paragraph in document.paragraphs:
        text = paragraph.text.strip()
        if text in optimization_map:
            new_text = optimization_map[text]
            if paragraph.runs:
                first_run = paragraph.runs[0]
                for run in paragraph.runs[1:]:
                    run.text = ""
                first_run.text = new_text
            else:
                paragraph.text = new_text

    output = BytesIO()
    document.save(output)
    output.seek(0)
    return output
Add the download endpoint

Now you need an endpoint that ties the whole pipeline together: parse the resume, send it to Gemini, then rebuild the document with the optimized text and return it as a downloadable file.

  • Switch back to main.py in VS Code.
  • Find this import line near the top:
from gemini_client import optimize_resume, suggest_templates
  • Add this new import directly below it:
from document_builder import build_optimized_document
  • Scroll to the bottom of main.py (below the /optimize endpoint) and add this new endpoint:
@app.post("/optimize/download")
async def optimize_download(file: UploadFile = File(...), job_description: str = Form(...)):
    # Read the uploaded file contents
    contents = await file.read()
    original_stream = BytesIO(contents)
    # Parse resume text for the AI prompt
    sections = parse_resume(BytesIO(contents))
    resume_text = "\n".join(
        f"## {s['heading']}\n" + "\n".join(s["bullets"])
        for s in sections
    )
    # Get optimized bullets from Gemini
    result = optimize_resume(resume_text, job_description)
    # Rebuild the document with optimized text
    original_stream.seek(0)
    output_stream = build_optimized_document(original_stream, result["optimized_bullets"])
    # Return as downloadable .docx file
    return StreamingResponse(
        output_stream,
        media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        headers={"Content-Disposition": "attachment; filename=optimized_resume.docx"},
    )

What does this endpoint do?

This endpoint runs the full pipeline in one request:

  • It reads the uploaded .docx bytes into two streams. One is for parsing text (consumed by the parser). The other is the original stream kept intact for document rebuilding.
  • original_stream.seek(0) rewinds the stream back to the start after the parser consumed it, so build_optimized_document can read from the beginning.
  • StreamingResponse sends the file back to the client as a download rather than inline JSON.
  • Save main.py. The dev server auto-reloads with your changes.

Seeing an import error in the terminal?

If the server logs show ModuleNotFoundError: No module named 'document_builder', confirm that document_builder.py is saved in the same folder as main.py. Both files must be at the root of your resume-optimizer directory.

help me fix a ModuleNotFoundError for document_builder in my FastAPI project

✔️ Awesome, I've got everything!

Great. Confirm you've saved main.py and that the terminal shows a successful server reload with no errors.

ⓧ I'd like to double check the full code

Your main.py now has five sections. Scroll through and confirm each is present in this order:

  • Imports at the top (including from document_builder import build_optimized_document).
  • The /parse endpoint.
  • The /analyze endpoint.
  • The /optimize endpoint.
  • The new /optimize/download endpoint at the bottom.
Test the download endpoint

Before you run this, what do you think will happen when you upload a resume and JD to this endpoint? Will you get JSON back, or something different?

  • Open http://127.0.0.1:8000/docs in your browser.
  • Find the POST /optimize/download endpoint and click Try it out.
  • Upload a .docx resume file in the file field.
  • Paste a job description into the job_description field (any real JD from a job posting works).
  • Click Execute.

Instead of a JSON response, Swagger UI shows a Download file link. Click it to save the .docx file to your computer. Open it in Word or Google Docs and confirm the formatting matches your original, but the bullet point text has been updated with ATS keywords from the job description.

What should I see?

The response shows a 200 status code and a download link for optimized_resume.docx. When you open the file, your headings, fonts, and layout look the same as the original. Only the bullet point text has changed to incorporate keywords from the JD.

Download link not appearing?

Swagger UI can sometimes struggle with file downloads from POST endpoints. If you see a response body with garbled characters instead of a download link, try using curl to confirm the endpoint works:

curl -X POST "http://127.0.0.1:8000/optimize/download" -F "file=@your-resume.docx" -F "job_description=Your JD text here" -o optimized_resume.docx

If the file downloads successfully via curl, your API is working correctly.

help me troubleshoot file downloads from my FastAPI StreamingResponse endpoint

Your API now handles the full loop: upload a resume, get it optimized by AI, and download the result as a properly formatted .docx file. Next up, you'll add a template suggestions endpoint that recommends resume structures based on a target role.

Build the Template Suggestions Endpoint

Your optimizer can rewrite bullet points, but what if a user's resume is poorly structured to begin with? Keyword stuffing won't help if the sections are in the wrong order or the format confuses ATS parsers.

In this step, you'll wire up the suggest_templates function you built earlier to a new GET endpoint. This gives users actionable structural advice for their target role before they even start optimizing content.

In this step, get ready to:
  • Add a GET /templates endpoint that accepts a role query parameter.
  • Test it with a real role and confirm you get structured template suggestions back.
Add the GET /templates endpoint

Your gemini_client.py already has the suggest_templates function ready to go. All you need now is a FastAPI route that calls it.

  • In main.py, scroll to the bottom of the file (below the /optimize/download endpoint) and add this new endpoint:
@app.get("/templates")
async def templates(role: str = Query(..., description="Target job role")):
    suggestions = suggest_templates(role)
    return {"role": role, "templates": suggestions}

What does this code do?

  • @app.get("/templates") registers this as a GET endpoint (not POST), because the user is requesting information rather than uploading a file.
  • Query(...) tells FastAPI to read the role value from the URL query string (e.g. /templates?role=backend engineer).
  • The function calls suggest_templates (which sends the role to Gemini with your templates system instruction) and returns the AI's structured response alongside the requested role.
  • Save main.py.

Your FastAPI dev server auto-reloads on save, so the new endpoint is already live.

Before you test this, what do you think the response will contain? You told Gemini to suggest 3 templates, each with sections, tips, and a summary format.

  • Switch back to your browser and open http://127.0.0.1:8000/docs.
  • Find the GET /templates endpoint in the docs list and click it to expand.
  • Click Try it out.
  • Enter backend engineer in the role field.
  • Click Execute.

You should see a JSON response containing 3 recommended resume structures. Each template includes a name, an ordered list of sections, formatting tips specific to the backend engineer role, and a sample summary format.

Not seeing the /templates endpoint?

  • Check your terminal for a reload error. A syntax mistake (missing colon, wrong indentation) prevents the server from restarting.
  • Make sure the endpoint is outside the optimize_download function body. It should be at the same indentation level as your other @app decorators.
  • Confirm you saved the file. Unsaved edits in VS Code show a dot on the file tab.

Help me debug my /templates endpoint not showing up in FastAPI docs.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and your server reloaded without errors.

ⓧ I'd like to double check the full code

Your main.py is now too long to show in one block. Scroll through your file and confirm these sections exist in order:

  • Imports at the top (os, BytesIO, FastAPI classes, your three module imports, and load_dotenv).
  • load_dotenv() call and app = FastAPI(title="ATS Resume Optimizer").
  • POST /parse endpoint.
  • POST /analyze endpoint.
  • POST /optimize endpoint.
  • POST /optimize/download endpoint.
  • GET /templates endpoint (the one you just added).

Your API now has five working endpoints covering the full resume optimization workflow. From parsing to AI analysis to document rebuilding to structural advice. Next up, you'll push this even further with a side-by-side comparison scorer in the Secret Mission.

Secret mission

Resume Comparison Scorer

Your optimizer rewrites resumes, but how much better are they really? Build a comparison endpoint that scores your original and optimized resumes side-by-side across four ATS categories, giving you hard numbers on exactly where the improvements landed.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a free-tier API key, so there are no ongoing costs.

Resources you used:

  • Gemini API key (free tier, no charges).
  • Local Python virtual environment and project files in the resume-optimizer folder.

Keep everything running

No action needed. Choose this if you're still actively building or plan to use your optimizer for upcoming job applications.

  • Everything is local on your machine. There are no running servers or cloud resources consuming credits.
  • Your Gemini API key uses the free tier with no billing attached. It stays active until you manually revoke it.
  • To use your API again later, open a terminal in the resume-optimizer folder, activate the virtual environment, and run fastapi dev.

Pause - I'll come back to this later

Stop the running dev server to free up your terminal. Your files and API key stay intact for next time.

  • In the terminal where your FastAPI server is running, press Ctrl+C to stop the server.
  • Your project files and virtual environment remain in the resume-optimizer folder. To resume later, activate the venv and run fastapi dev again.

Delete - I don't want to use this again

Remove all project files and revoke your API key to start completely fresh.

  • In the terminal where your FastAPI server is running, press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate
  • Delete your project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf resume-optimizer
  • On Windows, run:
cd ~\Desktop
rmdir /s /q resume-optimizer

Nice Work!

Nice Work!

You just built a production-grade resume optimization API from scratch. Upload any resume, paste a job description, and get back a tailored .docx file ready to submit.

You've learned how to:

  • Build a complete REST API with FastAPI that handles file uploads, form data, and streaming file downloads.
  • Integrate Google Gemini AI with structured JSON output to perform intelligent text analysis and keyword optimization.
  • Implement a full document processing pipeline that parses .docx files, rewrites content with AI, and rebuilds the document while preserving original formatting.
  • Secret Mission: Built a comparative scoring system that evaluates your original vs. optimized resume side-by-side across four ATS categories.

Ready to quiz yourself?