AI Resume Tailor with Google Gemini

Build a web app that uses Gemini AI to tailor and score resumes.

Introduction

30 Second Summary

Job applications are a numbers game, but customizing your resume for every single posting takes hours. What if AI could do the heavy lifting for you?

In this project, you will build ResuMatch, a local web application that uses Google's Gemini AI to rewrite your resume bullets for specific job descriptions and score how well your resume matches a role.

What You'll Build

A Python Flask web app with a clean browser interface that calls the Google Gemini API to tailor and score resumes against job descriptions.

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

  • A working Flask backend that sends structured prompts to Gemini and returns tailored resume text and JSON scores.
  • A browser-based frontend with two text areas and action buttons that displays AI-generated results in real time.
  • Hands-on experience with LLM API integration, prompt engineering, and structured outputs using Pydantic models.
  • Secret Mission: Add a cover letter generator that drafts targeted cover letters from your resume and the job description.

Are there any prerequisites?

You should have basic familiarity with Python syntax, HTML, and CSS. No prior experience with AI APIs or web frameworks is needed.

Install Python and Set Up Your Project

Before you can build ResuMatch, you need Python running on your machine and a clean workspace for your project files.

In this step, you will install Python 3.14, create a dedicated project folder, and install the two packages your app needs: the Google Gemini SDK and Flask (a lightweight web framework).

In this step, get ready to:
  • Install Python 3.14 and add it to your PATH.
  • Create a project folder with a virtual environment.
  • Install the google-genai and Flask packages.
Install Python 3.14

Python is the programming language you will use to build the backend of ResuMatch. You need version 3.14 or higher because the Google Gemini SDK requires Python 3.10+.

  • Check if Python is already installed by running this command in a terminal:
python --version

How do I open a terminal?

Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar. Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.

On macOS, you may need to use python3 --version instead.

✔️ I see version 3.14 or higher

Python is ready to go. Skip ahead to the next substep.

ⓧ I see an older version

You have Python installed, but you need version 3.14 for this project. Download the latest installer to upgrade.

  • Go to python.org/downloads in your browser.
  • Click the download button for Python 3.14. On Windows, choose the traditional installer (64-bit).
  • Run the installer. On the first screen, check the box labelled Add python.exe to PATH before clicking Install Now.
  • Once installation finishes, close and reopen your terminal so it picks up the new PATH.
  • Run python --version again to confirm it now shows 3.14.

macOS users with an older system Python

macOS often ships with an older Python version. After installing 3.14 from python.org, you may need to run python3.14 --version or use a version manager like pyenv to make the new version the default.

ⓧ Command not found

Python is not installed yet. Download and install it now.

  • Go to python.org/downloads in your browser.
  • Click the download button for Python 3.14. On Windows, choose the traditional installer (64-bit).
  • Run the installer. On the first screen, check the box labelled Add python.exe to PATH before clicking Install Now.
  • Once installation finishes, close and reopen your terminal so it picks up the new PATH.
  • Run python --version again to confirm it now shows 3.14.

Why check the PATH box?

Adding Python to your PATH lets you run python from any terminal window without typing the full installation path. Without it, your terminal would not recognise the python command.

Set up your project folder and install packages

A virtual environment gives your project its own isolated set of packages. This prevents conflicts with other Python projects on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open a terminal inside VS Code by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir resumatch
cd resumatch
  • Confirm the folder was created by running:
ls

You should see the resumatch folder listed on your Desktop. Since you already moved into it, the ls output will be empty (the folder is brand new).

  • Create a virtual environment by running:
python -m venv venv

What does this command do?

The python -m venv venv command creates a folder called venv inside your project. This folder contains a self-contained Python installation that keeps your project's packages separate from everything else on your machine.

  • Activate the virtual environment by running the command for your operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, PowerShell may block the activation script. Try running Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser first, then retry the activate command.

On macOS, if python was not found earlier, use python3 -m venv venv to recreate the environment, then activate again.

Why can't I activate my virtual environment?

  • Install the project dependencies by running:
pip install google-genai flask

What are these packages?

The google-genai package is the official SDK for calling Google's Gemini AI models from Python.

The flask package is a lightweight web framework that will serve your app's HTML page and handle API requests from the browser.

You should see output ending with a line that says Successfully installed followed by a list of package names.

Seeing a pip error?

Make sure (venv) appears in your prompt before running pip install. If it does not, your virtual environment is not active. Re-run the activate command from above.

If you see a permissions error, do not use sudo. Instead, make sure you created and activated the venv first.

Help me fix my pip install error

Verify your installation

A quick check confirms Python and the Gemini SDK are both working correctly inside your virtual environment.

  • Confirm your Python version by running:
python --version

You should see Python 3.14 followed by a patch number (for example 3.14.5).

  • Confirm the Gemini SDK is importable by running:
python -c "from google import genai; print('SDK ready')"

You should see SDK ready printed in your terminal. This proves the google-genai package is installed and importable.

Seeing ModuleNotFoundError?

This means the package did not install into your active environment. Check that (venv) still appears in your prompt. If it does, re-run pip install google-genai and look for error messages in the output.

Why is my import failing after pip install?

Your environment is locked and loaded. Next up, you will get a free Gemini API key and make your first AI call.

Get Your Free Gemini API Key

Your Python environment is set up and ready to go. Now you need a way to talk to Google's Gemini AI model.

Every API call requires an API key to authenticate your requests. Google AI Studio provides free Gemini API keys with no credit card and no expiration.

In this step, get ready to:
  • Get a free Gemini API key from Google AI Studio.
  • Store the key safely in your project folder.
  • Test the connection with a basic API call.
Sign in to Google AI Studio and create your API key

Google AI Studio is the portal where you manage your Gemini API keys. You only need a personal Google account to get started.

  • Open your web browser and go to aistudio.google.com.
  • Sign in with your Google account.
  • Click Get API key in the left sidebar.
  • Click Create API key in new project.
  • Copy the generated API key and save it somewhere temporarily (you will paste it into a file next).

Why a new project?

Google AI Studio creates a Google Cloud project behind the scenes to track your API usage. For a personal project like this one, creating a new project keeps things isolated and easy to manage.

Keep your key secret

Treat your API key like a password. Never commit it to version control or share it publicly. You will store it in a .env file that stays on your machine only.

Store your key and create the test script

You will create two files: a .env file to hold your key, and an app.py file to test the connection.

  • In VS Code, create a new file called .env in your project folder.
  • Paste the following into .env, replacing your-api-key-here with the key you copied from Google AI Studio:
GEMINI_API_KEY=your-api-key-here

What is a .env file?

A .env file stores environment variables for your project. It keeps sensitive values like API keys out of your code files so you never accidentally share them.

  • Save .env.

Now you need to load this key into your terminal session so Python can read it.

  • Set the environment variable in your terminal by running the appropriate command for your operating system:

macOS

export GEMINI_API_KEY=$(grep GEMINI_API_KEY .env | cut -d '=' -f2)

Windows

$env:GEMINI_API_KEY = (Get-Content .env | Select-String 'GEMINI_API_KEY') -replace 'GEMINI_API_KEY=',''

What does this command do?

This reads the API key value from your .env file and sets it as an environment variable in your current terminal session. Your Python code will then read the key using os.environ.get("GEMINI_API_KEY").

  • Create a new file called app.py in your project folder and paste the following code:
import os
from google import genai

# Create a Gemini API client using your key
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.0-flash"

# Send a simple test prompt to Gemini
response = client.models.generate_content(
    model=MODEL,
    contents="Say hello!",
)
print(response.text)

What does this code do?

This script connects to the Gemini API and sends a simple prompt.

  • os.environ.get("GEMINI_API_KEY") reads the API key from your terminal environment.
  • genai.Client(...) creates an authenticated connection to Google's servers.
  • client.models.generate_content(...) sends your prompt to the gemini-2.0-flash model and returns the AI's response.
  • Save app.py.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is what your .env should look like (with your own key):

GEMINI_API_KEY=your-api-key-here

Here is what your app.py should look like:

import os
from google import genai

client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.0-flash"

response = client.models.generate_content(
    model=MODEL,
    contents="Say hello!",
)
print(response.text)
Run your test script

Time to verify that everything is wired up correctly.

  • Run your test script by entering this command in the terminal:
python app.py
  • You should see a friendly greeting from Gemini printed in your terminal (something like "Hello! How can I help you today?").

Not seeing a response?

  • If you see an error about the API key being None or invalid, make sure you ran the export (macOS) or $env: (Windows) command in the same terminal session where you are running python app.py.
  • If you see a permission error, double check that you copied the full API key from Google AI Studio with no extra spaces.
  • If you see a module not found error for google.genai, confirm your virtual environment is still activated (you should see its name in your terminal prompt).

Still stuck? Help me debug my Gemini API connection.

Your API key works and you can talk to Gemini. Next up, you will build the resume tailoring logic that transforms this simple connection into a powerful resume optimization tool.

Build the Resume Tailoring Logic

Your API connection is live and returning responses from Gemini. Now it's time to build the core feature of ResuMatch: a function that rewrites resume bullets to better match a target job description.

The key to getting useful output from an LLM is writing a clear, structured prompt. You'll design a prompt that tells Gemini exactly how to rewrite the resume while keeping it truthful and well-formatted.

In this step, get ready to:
  • Create the tailor_resume() function with a structured prompt.
  • Test the function with sample resume data and a job description.
Create the tailor_resume() function

Right now, app.py sends a hardcoded "Say hello!" message to Gemini. You'll replace that test code with a reusable function that accepts a resume and job description, then asks Gemini to rewrite the resume to better match the role.

  • In app.py, find these lines:
response = client.models.generate_content(
    model=MODEL,
    contents="Say hello!",
)
print(response.text)
  • Delete those four lines entirely.
  • In their place, add the following tailor_resume() function:
def tailor_resume(resume: str, job_description: str) -> str:
    prompt = f"""You are an expert resume writer and career coach. Your task is to rewrite the resume below to better match the target job description.

Rules:
- Keep all facts truthful. Do not invent experience, metrics, or skills the candidate does not have.
- Naturally integrate keywords and phrases from the job description into the resume bullets.
- Preserve any existing metrics and numbers.
- Start each bullet point with a strong action verb.
- Reorder sections to put the most relevant experience first.
- Keep the same overall structure (sections, job titles, dates).
- If a bullet point is not relevant to the target role, reframe it to highlight transferable skills.

TARGET JOB DESCRIPTION:
{job_description}

ORIGINAL RESUME:
{resume}

Rewrite the resume below. Return only the improved resume text, nothing else."""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
    )
    return response.text

What does this code do?

The function takes two strings (your resume and a job description) and builds a detailed prompt for Gemini. The prompt uses an f-string to inject both inputs into a template.

The rules section is the heart of the prompt engineering. Each rule constrains Gemini's behaviour: keep facts truthful, use JD keywords naturally, preserve metrics, start bullets with action verbs. Without these guardrails, the AI might hallucinate new experience or produce generic output.

The function returns response.text directly, which is the raw text Gemini generates.

  • Save app.py.

You won't see a change yet. The next chunk adds a test that calls this function and prints its output.

Test the function with sample data

To verify the function works before building the full web app, you'll add a small test block that runs when you execute the script directly.

  • At the bottom of app.py, below the tailor_resume() function, add this test block:
if __name__ == "__main__":
    sample_resume = "Software Engineer with 3 years experience. Built REST APIs. Led team of 2."
    sample_jd = "Looking for a backend engineer with Python, REST API, and team leadership experience."
    print(tailor_resume(sample_resume, sample_jd))

What does this code do?

The if __name__ == "__main__": guard ensures this test code only runs when you execute app.py directly. Later, when Flask imports this file, the test block will be skipped automatically.

The sample resume and job description are intentionally short. They give Gemini enough context to demonstrate the tailoring without you needing to paste a full resume every time you test.

  • Save app.py.
  • Run the script by entering this command in your terminal:
python app.py
  • You should see a rewritten version of the sample resume printed in your terminal. The output will include stronger action verbs and keywords from the job description like "Python" and "backend."

Not seeing any output?

  • Make sure your virtual environment is still activated. You should see the environment name in your terminal prompt.
  • Check that your GEMINI_API_KEY environment variable is still set. If you opened a new terminal window, you may need to set it again.
  • Verify there are no indentation errors in your function. Python is whitespace-sensitive.

Help me debug why my tailor_resume function isn't returning output from the Gemini API.

✔️ Awesome, I've got everything!

Great. Double check you've saved app.py and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete app.py at this point:

import os
from google import genai

client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.0-flash"


def tailor_resume(resume: str, job_description: str) -> str:
    prompt = f"""You are an expert resume writer and career coach. Your task is to rewrite the resume below to better match the target job description.

Rules:
- Keep all facts truthful. Do not invent experience, metrics, or skills the candidate does not have.
- Naturally integrate keywords and phrases from the job description into the resume bullets.
- Preserve any existing metrics and numbers.
- Start each bullet point with a strong action verb.
- Reorder sections to put the most relevant experience first.
- Keep the same overall structure (sections, job titles, dates).
- If a bullet point is not relevant to the target role, reframe it to highlight transferable skills.

TARGET JOB DESCRIPTION:
{job_description}

ORIGINAL RESUME:
{resume}

Rewrite the resume below. Return only the improved resume text, nothing else."""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
    )
    return response.text


if __name__ == "__main__":
    sample_resume = "Software Engineer with 3 years experience. Built REST APIs. Led team of 2."
    sample_jd = "Looking for a backend engineer with Python, REST API, and team leadership experience."
    print(tailor_resume(sample_resume, sample_jd))

Your tailoring logic is working. Next up, you'll build a scoring function that rates how well a resume matches a job description using structured JSON output.

Build the Resume Scoring Logic

Your tailoring feature rewrites resume text, but it gives no measurable feedback. A job seeker also needs to know how well their resume matches a posting before they submit it.

In this step, you will build a scoring function that rates a resume against a job description across multiple categories. The key difference from the tailor function is that scoring needs to return structured data, not free-form text.

In this step, get ready to:
  • Define Pydantic models that describe the exact shape of the AI's response.
  • Create a scoring function that uses Gemini's structured output feature to return reliable JSON.
  • Test the scorer with sample data and verify you get properly structured results.
Define the Pydantic models for structured output

When you call an LLM, it normally returns free-form text. For a scoring feature, you need the AI to return specific fields (like a number out of 100) in a predictable format that your code can parse.

What is structured output?

Structured output tells the AI model to respond in a specific JSON shape that you define. Instead of hoping the model formats its answer correctly, you give it a schema and the API guarantees the response matches.

The Gemini API accepts Pydantic models as schemas. Pydantic is a Python library that defines data shapes using type hints.

  • In app.py, find the import line at the top of the file:
import os
from google import genai
  • Replace those two lines with the following expanded imports:
import os
from google import genai
from pydantic import BaseModel, Field
from typing import List

What are these new imports?

BaseModel is the Pydantic class you extend to define a data shape. Field lets you add descriptions to each property so the AI knows what to put there.

List from Python's typing module tells Pydantic that one of your fields holds a list of objects.

  • Below the MODEL = "gemini-2.0-flash" line, add the two Pydantic model classes by pasting this code:
class Suggestion(BaseModel):
    category: str = Field(description="The category this suggestion belongs to")
    suggestion: str = Field(description="A specific, actionable suggestion")


class ResumeScore(BaseModel):
    overall_score: int = Field(description="Overall match score from 0 to 100")
    keyword_match: int = Field(description="Score for keyword alignment from 0 to 100")
    impact_score: int = Field(description="Score for quantified impact and metrics from 0 to 100")
    relevance_score: int = Field(description="Score for experience relevance from 0 to 100")
    summary: str = Field(description="A brief summary of the resume's strengths and weaknesses for this role")
    suggestions: List[Suggestion] = Field(description="List of specific suggestions to improve the resume for this role")

What do these models define?

Suggestion is a small model with two string fields: a category label and the suggestion text.

ResumeScore is the main response shape. It has four integer scores (0 to 100), a text summary, and a list of Suggestion objects. When you pass this schema to Gemini, the API guarantees the response matches these fields exactly.

  • Save app.py.

Seeing a red underline on the imports?

Pydantic is already installed as a dependency of google-genai, so you do not need to install it separately. If your editor shows an import error, make sure your VS Code Python interpreter is set to the virtual environment you created earlier.

Help me fix my Pydantic import error in VS Code.

Create the score_resume() function

This function sends the resume and job description to Gemini with instructions to evaluate and score the match. The key difference from your tailor function is the config parameter that forces the response into your Pydantic schema.

  • In app.py, add the following function below the tailor_resume() function (after return response.text):
def score_resume(resume: str, job_description: str) -> dict:
    prompt = f"""You are an expert ATS (Applicant Tracking System) analyst and hiring manager. Evaluate the resume below against the target job description.

Score the resume on these criteria (0-100 for each):
- keyword_match: How well does the resume incorporate keywords and skills from the job description?
- impact_score: Does the resume use quantified metrics and demonstrate measurable impact?
- relevance_score: How relevant is the candidate's experience to this specific role?

Also provide an overall_score (0-100), a brief summary of strengths and weaknesses, and a list of specific actionable suggestions to improve the match.

TARGET JOB DESCRIPTION:
{job_description}

RESUME:
{resume}"""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
        config={
            "response_format": {
                "text": {
                    "mime_type": "application/json",
                    "schema": ResumeScore.model_json_schema(),
                }
            },
        },
    )
    return ResumeScore.model_validate_json(response.text).model_dump()

What does this function do?

The prompt tells Gemini to act as an ATS analyst and score the resume on three criteria plus an overall score.

The config dictionary is the structured output configuration. ResumeScore.model_json_schema() converts your Pydantic class into a JSON Schema that the API enforces.

The return line does two things: model_validate_json() parses and validates the raw JSON string into a Pydantic object, and model_dump() converts that object into a plain Python dictionary you can easily work with.

  • Save app.py.

Not sure where to place this function?

The score_resume() function goes directly after the tailor_resume() function and before the if __name__ == "__main__": block at the bottom. Leave a blank line between each function for readability.

Help me figure out where to place score_resume in my file.

Test the scoring function

Now you will update the test block at the bottom of app.py to call your new scoring function instead of the tailor function.

  • In app.py, scroll to the if __name__ == "__main__": block at the bottom of the file.
  • Find this line:
    print(tailor_resume(sample_resume, sample_jd))
  • Replace tailor_resume with score_resume, so the line now looks like this:
    print(score_resume(sample_resume, sample_jd))
  • Save app.py, then run the script by entering this command in your terminal:
python app.py

You should see a dictionary printed in your terminal with fields like overall_score, keyword_match, impact_score, relevance_score, summary, and a suggestions list.

What should I see?

The output is a Python dictionary with integer scores and text feedback. The exact scores will vary each time because the AI interprets the sample differently, but the structure is always consistent thanks to your Pydantic schema.

Getting a validation error or empty response?

Make sure your .env file has a valid GEMINI_API_KEY and that you are running the script from the same folder as .env.

If you see a JSON parsing error, double-check that the config dictionary in your score_resume() function matches the code above exactly. A missing comma or mismatched brace will break the structured output request.

Help me debug my score_resume function error.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py and that your terminal printed a structured dictionary with scores.

ⓧ I'd like to double check the full code

import os
from google import genai
from pydantic import BaseModel, Field
from typing import List

client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.0-flash"


class Suggestion(BaseModel):
    category: str = Field(description="The category this suggestion belongs to")
    suggestion: str = Field(description="A specific, actionable suggestion")


class ResumeScore(BaseModel):
    overall_score: int = Field(description="Overall match score from 0 to 100")
    keyword_match: int = Field(description="Score for keyword alignment from 0 to 100")
    impact_score: int = Field(description="Score for quantified impact and metrics from 0 to 100")
    relevance_score: int = Field(description="Score for experience relevance from 0 to 100")
    summary: str = Field(description="A brief summary of the resume's strengths and weaknesses for this role")
    suggestions: List[Suggestion] = Field(description="List of specific suggestions to improve the resume for this role")


def tailor_resume(resume: str, job_description: str) -> str:
    prompt = f"""You are an expert resume writer and career coach. Your task is to rewrite the resume below to better match the target job description.

Rules:
- Keep all facts truthful. Do not invent experience, metrics, or skills the candidate does not have.
- Naturally integrate keywords and phrases from the job description into the resume bullets.
- Preserve any existing metrics and numbers.
- Start each bullet point with a strong action verb.
- Reorder sections to put the most relevant experience first.
- Keep the same overall structure (sections, job titles, dates).
- If a bullet point is not relevant to the target role, reframe it to highlight transferable skills.

TARGET JOB DESCRIPTION:
{job_description}

ORIGINAL RESUME:
{resume}

Rewrite the resume below. Return only the improved resume text, nothing else."""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
    )
    return response.text


def score_resume(resume: str, job_description: str) -> dict:
    prompt = f"""You are an expert ATS (Applicant Tracking System) analyst and hiring manager. Evaluate the resume below against the target job description.

Score the resume on these criteria (0-100 for each):
- keyword_match: How well does the resume incorporate keywords and skills from the job description?
- impact_score: Does the resume use quantified metrics and demonstrate measurable impact?
- relevance_score: How relevant is the candidate's experience to this specific role?

Also provide an overall_score (0-100), a brief summary of strengths and weaknesses, and a list of specific actionable suggestions to improve the match.

TARGET JOB DESCRIPTION:
{job_description}

RESUME:
{resume}"""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
        config={
            "response_format": {
                "text": {
                    "mime_type": "application/json",
                    "schema": ResumeScore.model_json_schema(),
                }
            },
        },
    )
    return ResumeScore.model_validate_json(response.text).model_dump()


if __name__ == "__main__":
    sample_resume = "Software Engineer with 3 years experience. Built REST APIs. Led team of 2."
    sample_jd = "Looking for a backend engineer with Python, REST API, and team leadership experience."
    print(score_resume(sample_resume, sample_jd))

Your scoring engine is working. Next up, you will wrap both features in a Flask web server so you can use them from a browser.

Build the Flask Web Server

Your AI functions can tailor resumes and score them against job descriptions. Right now, though, the only way to use them is by editing Python code directly.

In this step, you'll wrap those functions in a Flask web server and build a frontend interface. By the end, you'll have a proper web app with text inputs, buttons, and a results display area.

In this step, get ready to:
  • Set up Flask routes that expose your tailor and score functions as API endpoints.
  • Create an HTML page with textareas and buttons for user input.
  • Add CSS styling and JavaScript to handle requests and display results.
Set up the Flask app and API routes

Flask turns Python functions into HTTP endpoints that a browser can call. You'll add three routes: one to serve the HTML page, and two POST endpoints for the tailor and score features.

  • In app.py, find the import lines at the top of the file:
import os
from google import genai
  • Replace those two lines with the following updated imports that include Flask, and add the app creation line directly below:
import os
from flask import Flask, request, jsonify, send_from_directory
from google import genai
from pydantic import BaseModel, Field
from typing import List

app = Flask(__name__, static_folder="static")

What does this code do?

The from flask import ... line brings in the tools you need: Flask creates the app, request reads incoming data, jsonify sends JSON responses, and send_from_directory serves static files.

The app = Flask(__name__, static_folder="static") line creates your Flask application and tells it to look for HTML, CSS, and JS files inside a folder called static.

  • Now scroll to the bottom of app.py and find the old test block:
if __name__ == "__main__":
    sample_resume = "Software Engineer with 3 years experience. Built REST APIs. Led team of 2."
    sample_jd = "Looking for a backend engineer with Python, REST API, and team leadership experience."
    print(score_resume(sample_resume, sample_jd))
  • Delete that entire block and replace it with the following route definitions:
@app.route("/")
def index():
    return send_from_directory("static", "index.html")


@app.route("/tailor", methods=["POST"])
def tailor():
    data = request.get_json()
    resume = data.get("resume", "")
    job_description = data.get("job_description", "")

    if not resume or not job_description:
        return jsonify({"error": "Both resume and job description are required"}), 400

    result = tailor_resume(resume, job_description)
    return jsonify({"result": result})


@app.route("/score", methods=["POST"])
def score():
    data = request.get_json()
    resume = data.get("resume", "")
    job_description = data.get("job_description", "")

    if not resume or not job_description:
        return jsonify({"error": "Both resume and job description are required"}), 400

    result = score_resume(resume, job_description)
    return jsonify({"result": result})


if __name__ == "__main__":
    app.run(debug=True)

What do these routes do?

  • The / route serves your HTML page when someone visits the app in their browser.
  • The /tailor and /score routes accept POST requests with JSON data. They extract the resume and job description, call the matching AI function, and return the result as JSON.
  • Both routes check that the inputs are not empty and return a 400 error if they are.
  • The app.run(debug=True) line starts the development server with auto-reload enabled.
  • Save app.py.

Seeing a red underline on the Flask import?

Make sure your virtual environment is activated. If you see import errors in VS Code, check that the Python interpreter in the bottom-left corner matches your virtual environment. Help me fix my Flask import error in VS Code.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

import os
from flask import Flask, request, jsonify, send_from_directory
from google import genai
from pydantic import BaseModel, Field
from typing import List

app = Flask(__name__, static_folder="static")

client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
MODEL = "gemini-2.0-flash"


class Suggestion(BaseModel):
    category: str = Field(description="The category this suggestion belongs to")
    suggestion: str = Field(description="A specific, actionable suggestion")


class ResumeScore(BaseModel):
    overall_score: int = Field(description="Overall match score from 0 to 100")
    keyword_match: int = Field(description="Score for keyword alignment from 0 to 100")
    impact_score: int = Field(description="Score for quantified impact and metrics from 0 to 100")
    relevance_score: int = Field(description="Score for experience relevance from 0 to 100")
    summary: str = Field(description="A brief summary of the resume's strengths and weaknesses for this role")
    suggestions: List[Suggestion] = Field(description="List of specific suggestions to improve the resume for this role")


def tailor_resume(resume: str, job_description: str) -> str:
    prompt = f"""You are an expert resume writer and career coach. Your task is to rewrite the resume below to better match the target job description.

Rules:
- Keep all facts truthful. Do not invent experience, metrics, or skills the candidate does not have.
- Naturally integrate keywords and phrases from the job description into the resume bullets.
- Preserve any existing metrics and numbers.
- Start each bullet point with a strong action verb.
- Reorder sections to put the most relevant experience first.
- Keep the same overall structure (sections, job titles, dates).
- If a bullet point is not relevant to the target role, reframe it to highlight transferable skills.

TARGET JOB DESCRIPTION:
{job_description}

ORIGINAL RESUME:
{resume}

Rewrite the resume below. Return only the improved resume text, nothing else."""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
    )
    return response.text


def score_resume(resume: str, job_description: str) -> dict:
    prompt = f"""You are an expert ATS (Applicant Tracking System) analyst and hiring manager. Evaluate the resume below against the target job description.

Score the resume on these criteria (0-100 for each):
- keyword_match: How well does the resume incorporate keywords and skills from the job description?
- impact_score: Does the resume use quantified metrics and demonstrate measurable impact?
- relevance_score: How relevant is the candidate's experience to this specific role?

Also provide an overall_score (0-100), a brief summary of strengths and weaknesses, and a list of specific actionable suggestions to improve the match.

TARGET JOB DESCRIPTION:
{job_description}

RESUME:
{resume}"""

    response = client.models.generate_content(
        model=MODEL,
        contents=prompt,
        config={
            "response_format": {
                "text": {
                    "mime_type": "application/json",
                    "schema": ResumeScore.model_json_schema(),
                }
            },
        },
    )
    return ResumeScore.model_validate_json(response.text).model_dump()


@app.route("/")
def index():
    return send_from_directory("static", "index.html")


@app.route("/tailor", methods=["POST"])
def tailor():
    data = request.get_json()
    resume = data.get("resume", "")
    job_description = data.get("job_description", "")

    if not resume or not job_description:
        return jsonify({"error": "Both resume and job description are required"}), 400

    result = tailor_resume(resume, job_description)
    return jsonify({"result": result})


@app.route("/score", methods=["POST"])
def score():
    data = request.get_json()
    resume = data.get("resume", "")
    job_description = data.get("job_description", "")

    if not resume or not job_description:
        return jsonify({"error": "Both resume and job description are required"}), 400

    result = score_resume(resume, job_description)
    return jsonify({"result": result})


if __name__ == "__main__":
    app.run(debug=True)
Create the HTML page

Your Flask app needs a frontend that users interact with. You'll create a static folder and an HTML file inside it with textareas for input and buttons to trigger the AI features.

  • Create the static folder inside your project directory by running this command:
mkdir static
  • Confirm the folder exists by running:
ls

You should see static listed alongside app.py and .env.

  • Create a new file called index.html inside the static folder. In VS Code, right-click the static folder in the Explorer sidebar and select New File.
  • Name the file index.html and paste the following code into it:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ResuMatch - AI Resume Tailor &amp; Scorer</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>ResuMatch</h1>
            <p class="subtitle">AI-powered resume tailoring and scoring</p>
        </header>

        <div class="input-section">
            <div class="input-group">
                <label for="resume">Your Resume</label>
                <textarea id="resume" placeholder="Paste your resume text here..."></textarea>
            </div>
            <div class="input-group">
                <label for="job-description">Job Description</label>
                <textarea id="job-description" placeholder="Paste the target job description here..."></textarea>
            </div>
        </div>

        <div class="actions">
            <button id="tailor-btn" onclick="handleTailor()">Tailor Resume</button>
            <button id="score-btn" onclick="handleScore()">Score Resume</button>
        </div>

        <div id="loading" class="loading hidden">Analyzing with AI...</div>

        <div id="results" class="results hidden">
            <h2>Results</h2>
            <div id="results-content"></div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

What does this HTML do?

  • Two <textarea> elements give the user space to paste their resume and a job description.
  • Two buttons call handleTailor() and handleScore() when clicked. These JavaScript functions will send data to your Flask API.
  • A hidden loading div shows a message while the AI processes. A hidden results div displays the output once it arrives.
  • Save static/index.html.

File not appearing in the static folder?

Make sure you created the file inside the static folder, not in the project root. In VS Code's Explorer sidebar, the file should appear nested under static/. Help me figure out why my index.html isn't in the right folder.

✔️ Awesome, I've got everything!

Great. Make sure you've saved static/index.html before moving on.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ResuMatch - AI Resume Tailor &amp; Scorer</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>ResuMatch</h1>
            <p class="subtitle">AI-powered resume tailoring and scoring</p>
        </header>

        <div class="input-section">
            <div class="input-group">
                <label for="resume">Your Resume</label>
                <textarea id="resume" placeholder="Paste your resume text here..."></textarea>
            </div>
            <div class="input-group">
                <label for="job-description">Job Description</label>
                <textarea id="job-description" placeholder="Paste the target job description here..."></textarea>
            </div>
        </div>

        <div class="actions">
            <button id="tailor-btn" onclick="handleTailor()">Tailor Resume</button>
            <button id="score-btn" onclick="handleScore()">Score Resume</button>
        </div>

        <div id="loading" class="loading hidden">Analyzing with AI...</div>

        <div id="results" class="results hidden">
            <h2>Results</h2>
            <div id="results-content"></div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>
Add styling and JavaScript

The HTML structure is in place, but it needs visual styling and client-side logic. You'll create two more files: a CSS stylesheet for layout and appearance, and a JavaScript file to handle button clicks and API requests.

  • Create a new file called style.css inside the static folder. Right-click static in VS Code's Explorer and select New File.
  • Name the file style.css and paste the following code into it:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #f5f7fa;
    color: #333;
    line-height: 1.6;
}

.container {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem;
}

header {
    text-align: center;
    margin-bottom: 2rem;
}

header h1 {
    font-size: 2.5rem;
    color: #2d3748;
}

.subtitle {
    color: #718096;
    font-size: 1.1rem;
}

.input-section {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}

.input-group {
    display: flex;
    flex-direction: column;
}

.input-group label {
    font-weight: 600;
    margin-bottom: 0.5rem;
    color: #4a5568;
}

textarea {
    width: 100%;
    height: 300px;
    padding: 1rem;
    border: 2px solid #e2e8f0;
    border-radius: 8px;
    font-family: inherit;
    font-size: 0.9rem;
    resize: vertical;
    transition: border-color 0.2s;
}

textarea:focus {
    outline: none;
    border-color: #4299e1;
}

.actions {
    display: flex;
    gap: 1rem;
    justify-content: center;
    margin-bottom: 2rem;
}

button {
    padding: 0.75rem 2rem;
    font-size: 1rem;
    font-weight: 600;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: background-color 0.2s, transform 0.1s;
}

button:active {
    transform: scale(0.98);
}

#tailor-btn {
    background-color: #4299e1;
    color: white;
}

#tailor-btn:hover {
    background-color: #3182ce;
}

#score-btn {
    background-color: #48bb78;
    color: white;
}

#score-btn:hover {
    background-color: #38a169;
}

button:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.loading {
    text-align: center;
    padding: 2rem;
    font-size: 1.1rem;
    color: #718096;
}

.results {
    background: white;
    border-radius: 12px;
    padding: 2rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}

.results h2 {
    margin-bottom: 1rem;
    color: #2d3748;
}

.hidden {
    display: none;
}

.score-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    margin-bottom: 1.5rem;
}

.score-card {
    background: #f7fafc;
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
}

.score-card .score-value {
    font-size: 2rem;
    font-weight: 700;
    color: #2d3748;
}

.score-card .score-label {
    font-size: 0.85rem;
    color: #718096;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.summary-text {
    padding: 1rem;
    background: #f7fafc;
    border-radius: 8px;
    margin-bottom: 1rem;
}

.suggestions-list {
    list-style: none;
    padding: 0;
}

.suggestions-list li {
    padding: 0.75rem 1rem;
    margin-bottom: 0.5rem;
    background: #fffaf0;
    border-left: 4px solid #ed8936;
    border-radius: 4px;
}

.tailored-resume {
    white-space: pre-wrap;
    padding: 1.5rem;
    background: #f7fafc;
    border-radius: 8px;
    font-size: 0.9rem;
    line-height: 1.7;
}

What does this stylesheet do?

The CSS sets up a centered layout with a two-column grid for the textareas. It styles the buttons with distinct colors (blue for Tailor, green for Score) and adds a card-style results section.

The .hidden class uses display: none to keep the loading indicator and results area invisible until JavaScript shows them.

  • Save static/style.css.

✔️ Awesome, I've got everything!

Great. Make sure you've saved static/style.css before moving on to the JavaScript file.

ⓧ I'd like to double check the full code

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #f5f7fa;
    color: #333;
    line-height: 1.6;
}

.container {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem;
}

header {
    text-align: center;
    margin-bottom: 2rem;
}

header h1 {
    font-size: 2.5rem;
    color: #2d3748;
}

.subtitle {
    color: #718096;
    font-size: 1.1rem;
}

.input-section {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}

.input-group {
    display: flex;
    flex-direction: column;
}

.input-group label {
    font-weight: 600;
    margin-bottom: 0.5rem;
    color: #4a5568;
}

textarea {
    width: 100%;
    height: 300px;
    padding: 1rem;
    border: 2px solid #e2e8f0;
    border-radius: 8px;
    font-family: inherit;
    font-size: 0.9rem;
    resize: vertical;
    transition: border-color 0.2s;
}

textarea:focus {
    outline: none;
    border-color: #4299e1;
}

.actions {
    display: flex;
    gap: 1rem;
    justify-content: center;
    margin-bottom: 2rem;
}

button {
    padding: 0.75rem 2rem;
    font-size: 1rem;
    font-weight: 600;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: background-color 0.2s, transform 0.1s;
}

button:active {
    transform: scale(0.98);
}

#tailor-btn {
    background-color: #4299e1;
    color: white;
}

#tailor-btn:hover {
    background-color: #3182ce;
}

#score-btn {
    background-color: #48bb78;
    color: white;
}

#score-btn:hover {
    background-color: #38a169;
}

button:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.loading {
    text-align: center;
    padding: 2rem;
    font-size: 1.1rem;
    color: #718096;
}

.results {
    background: white;
    border-radius: 12px;
    padding: 2rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}

.results h2 {
    margin-bottom: 1rem;
    color: #2d3748;
}

.hidden {
    display: none;
}

.score-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    margin-bottom: 1.5rem;
}

.score-card {
    background: #f7fafc;
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
}

.score-card .score-value {
    font-size: 2rem;
    font-weight: 700;
    color: #2d3748;
}

.score-card .score-label {
    font-size: 0.85rem;
    color: #718096;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.summary-text {
    padding: 1rem;
    background: #f7fafc;
    border-radius: 8px;
    margin-bottom: 1rem;
}

.suggestions-list {
    list-style: none;
    padding: 0;
}

.suggestions-list li {
    padding: 0.75rem 1rem;
    margin-bottom: 0.5rem;
    background: #fffaf0;
    border-left: 4px solid #ed8936;
    border-radius: 4px;
}

.tailored-resume {
    white-space: pre-wrap;
    padding: 1.5rem;
    background: #f7fafc;
    border-radius: 8px;
    font-size: 0.9rem;
    line-height: 1.7;
}
  • Create another new file inside static called script.js. Right-click static in VS Code's Explorer and select New File.
  • Name the file script.js and paste the following code into it:
async function handleTailor() {
    const resume = document.getElementById("resume").value;
    const jobDescription = document.getElementById("job-description").value;

    if (!resume || !jobDescription) {
        alert("Please fill in both the resume and job description fields.");
        return;
    }

    setLoading(true);

    try {
        const response = await fetch("/tailor", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ resume, job_description: jobDescription }),
        });

        const data = await response.json();

        if (response.ok) {
            showTailorResults(data.result);
        } else {
            showError(data.error || "Something went wrong.");
        }
    } catch (error) {
        showError("Failed to connect to the server.");
    } finally {
        setLoading(false);
    }
}

async function handleScore() {
    const resume = document.getElementById("resume").value;
    const jobDescription = document.getElementById("job-description").value;

    if (!resume || !jobDescription) {
        alert("Please fill in both the resume and job description fields.");
        return;
    }

    setLoading(true);

    try {
        const response = await fetch("/score", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ resume, job_description: jobDescription }),
        });

        const data = await response.json();

        if (response.ok) {
            showScoreResults(data.result);
        } else {
            showError(data.error || "Something went wrong.");
        }
    } catch (error) {
        showError("Failed to connect to the server.");
    } finally {
        setLoading(false);
    }
}

function setLoading(isLoading) {
    const loading = document.getElementById("loading");
    const buttons = document.querySelectorAll("button");

    if (isLoading) {
        loading.classList.remove("hidden");
        buttons.forEach(btn => btn.disabled = true);
    } else {
        loading.classList.add("hidden");
        buttons.forEach(btn => btn.disabled = false);
    }
}

function showTailorResults(text) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    content.innerHTML = `<div class="tailored-resume">${escapeHtml(text)}</div>`;
    results.classList.remove("hidden");
}

function showScoreResults(score) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    let suggestionsHtml = "";
    if (score.suggestions && score.suggestions.length > 0) {
        suggestionsHtml = `
            <h3>Suggestions</h3>
            <ul class="suggestions-list">
                ${score.suggestions.map(s => `<li><strong>${escapeHtml(s.category)}:</strong> ${escapeHtml(s.suggestion)}</li>`).join("")}
            </ul>
        `;
    }

    content.innerHTML = `
        <div class="score-grid">
            <div class="score-card">
                <div class="score-value">${score.overall_score}</div>
                <div class="score-label">Overall Score</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.keyword_match}</div>
                <div class="score-label">Keyword Match</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.impact_score}</div>
                <div class="score-label">Impact &amp; Metrics</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.relevance_score}</div>
                <div class="score-label">Relevance</div>
            </div>
        </div>
        <div class="summary-text">${escapeHtml(score.summary)}</div>
        ${suggestionsHtml}
    `;
    results.classList.remove("hidden");
}

function showError(message) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    content.innerHTML = `<p style="color: #e53e3e;">${escapeHtml(message)}</p>`;
    results.classList.remove("hidden");
}

function escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
}

What does this JavaScript do?

  • handleTailor() and handleScore() read the textarea values, send them to the Flask API as JSON POST requests, and display the response.
  • setLoading() shows the loading indicator and disables buttons while waiting for the AI response.
  • showScoreResults() builds a score card grid with the structured JSON data that comes back from Gemini.
  • escapeHtml() prevents any HTML in the AI response from being rendered as actual markup. This keeps the display safe.
  • Save static/script.js.
  • In VS Code's Explorer sidebar, expand the static folder. You should see three files: index.html, style.css, and script.js.

Missing a file in the static folder?

Double-check that all three files are directly inside static/ and not nested in a subfolder. The file names must be exactly index.html, style.css, and script.js (all lowercase). Help me check my static folder structure.

✔️ Awesome, I've got everything!

Great. Make sure all three files in static/ are saved before moving on.

ⓧ I'd like to double check the full code

async function handleTailor() {
    const resume = document.getElementById("resume").value;
    const jobDescription = document.getElementById("job-description").value;

    if (!resume || !jobDescription) {
        alert("Please fill in both the resume and job description fields.");
        return;
    }

    setLoading(true);

    try {
        const response = await fetch("/tailor", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ resume, job_description: jobDescription }),
        });

        const data = await response.json();

        if (response.ok) {
            showTailorResults(data.result);
        } else {
            showError(data.error || "Something went wrong.");
        }
    } catch (error) {
        showError("Failed to connect to the server.");
    } finally {
        setLoading(false);
    }
}

async function handleScore() {
    const resume = document.getElementById("resume").value;
    const jobDescription = document.getElementById("job-description").value;

    if (!resume || !jobDescription) {
        alert("Please fill in both the resume and job description fields.");
        return;
    }

    setLoading(true);

    try {
        const response = await fetch("/score", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ resume, job_description: jobDescription }),
        });

        const data = await response.json();

        if (response.ok) {
            showScoreResults(data.result);
        } else {
            showError(data.error || "Something went wrong.");
        }
    } catch (error) {
        showError("Failed to connect to the server.");
    } finally {
        setLoading(false);
    }
}

function setLoading(isLoading) {
    const loading = document.getElementById("loading");
    const buttons = document.querySelectorAll("button");

    if (isLoading) {
        loading.classList.remove("hidden");
        buttons.forEach(btn => btn.disabled = true);
    } else {
        loading.classList.add("hidden");
        buttons.forEach(btn => btn.disabled = false);
    }
}

function showTailorResults(text) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    content.innerHTML = `<div class="tailored-resume">${escapeHtml(text)}</div>`;
    results.classList.remove("hidden");
}

function showScoreResults(score) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    let suggestionsHtml = "";
    if (score.suggestions && score.suggestions.length > 0) {
        suggestionsHtml = `
            <h3>Suggestions</h3>
            <ul class="suggestions-list">
                ${score.suggestions.map(s => `<li><strong>${escapeHtml(s.category)}:</strong> ${escapeHtml(s.suggestion)}</li>`).join("")}
            </ul>
        `;
    }

    content.innerHTML = `
        <div class="score-grid">
            <div class="score-card">
                <div class="score-value">${score.overall_score}</div>
                <div class="score-label">Overall Score</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.keyword_match}</div>
                <div class="score-label">Keyword Match</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.impact_score}</div>
                <div class="score-label">Impact &amp; Metrics</div>
            </div>
            <div class="score-card">
                <div class="score-value">${score.relevance_score}</div>
                <div class="score-label">Relevance</div>
            </div>
        </div>
        <div class="summary-text">${escapeHtml(score.summary)}</div>
        ${suggestionsHtml}
    `;
    results.classList.remove("hidden");
}

function showError(message) {
    const results = document.getElementById("results");
    const content = document.getElementById("results-content");

    content.innerHTML = `<p style="color: #e53e3e;">${escapeHtml(message)}</p>`;
    results.classList.remove("hidden");
}

function escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
}

Your web server is fully wired up. The backend has routes, the frontend has a form, and the JavaScript connects the two. Next, you'll launch the server and test both AI features with real resume data.

Test Your Complete App

Your frontend and backend are built and connected. The Flask server knows how to receive requests from the browser, send them to Gemini, and return structured results.

Now it's time to bring everything together. You'll start the server, open ResuMatch in your browser, and test both the Tailor and Score features with real input.

In this step, get ready to:
  • Start the Flask development server with your API key loaded.
  • Test the Tailor feature with a sample resume and job description.
  • Test the Score feature and verify structured JSON scores appear.
Start the Flask development server

Your app.py reads the GEMINI_API_KEY from your system's environment variables. You need to load that value into your terminal session before starting the server.

macOS

  • Load your API key into the terminal session and start the server by running these commands:
export GEMINI_API_KEY=your-api-key-here
python app.py

What do these commands do?

  • export GEMINI_API_KEY=... sets the environment variable for your current terminal session. Replace your-api-key-here with the actual key from your .env file.
  • python app.py starts the Flask development server using the if __name__ == "__main__" block at the bottom of your file.

Windows

  • Load your API key into the terminal session and start the server by running these commands:
set GEMINI_API_KEY=your-api-key-here
python app.py

What do these commands do?

  • set GEMINI_API_KEY=... sets the environment variable for your current terminal session. Replace your-api-key-here with the actual key from your .env file.
  • python app.py starts the Flask development server using the if __name__ == "__main__" block at the bottom of your file.
  • You should see output similar to:
 * Serving Flask app 'app'
 * Debug mode: on
 * Running on http://127.0.0.1:5000
  • Open your web browser and navigate to http://127.0.0.1:5000.
  • You should see the ResuMatch interface with two text areas and two buttons.

Don't see the app in your browser?

  • Make sure the terminal still shows the server running. If you see an error, check that your GEMINI_API_KEY value is correct.
  • Confirm you're visiting http://127.0.0.1:5000 (not https).
  • If you see a "ModuleNotFoundError", your virtual environment might not be activated. Activate it again and rerun the commands above.

My Flask server won't start or the page won't load. Can you help me debug?

Test the Tailor feature

The Tailor feature rewrites your resume bullets to incorporate keywords from a job description. You'll paste in sample text and confirm the AI produces a rewritten version.

  • In the Your Resume text area on the left, paste the following sample resume:
EXPERIENCE

Software Developer, TechCorp (2022-2024)
- Built web applications using JavaScript and React
- Managed team of 3 developers on internal tools project
- Reduced page load time by 40% through code optimization
- Created automated testing suite covering 80% of codebase

Junior Developer, StartupXYZ (2020-2022)
- Developed REST APIs using Python and Flask
- Maintained PostgreSQL databases with 500K+ records
- Participated in code reviews and sprint planning
  • In the Job Description text area on the right, paste this sample job posting:
Senior Backend Engineer - CloudScale Inc.

We're looking for a Senior Backend Engineer to design and maintain scalable microservices. You'll work with Python, FastAPI, and AWS to build high-throughput data pipelines.

Requirements:
- 3+ years Python backend development
- Experience with cloud services (AWS preferred)
- Strong understanding of REST API design and microservices architecture
- Familiarity with CI/CD pipelines and containerization (Docker)
- Track record of optimizing system performance
  • Click the Tailor Resume button.
  • Wait for the "Analyzing with AI..." message to disappear.

After a few seconds, you should see a rewritten version of the resume appear in the Results section below the buttons.

What should I look for in the output?

The tailored resume should incorporate keywords from the job description like "microservices," "scalable," "AWS," and "data pipelines." The original facts (40% page load improvement, 80% code coverage, 500K+ records) should remain unchanged.

If the AI invents experience you didn't list, that's a sign the prompt rules are working differently than expected. The output should only reframe existing experience, not fabricate new skills.

Seeing an error instead of results?

  • If you see "Failed to connect to the server," make sure your Flask terminal is still running.
  • If you see a 500 error, check your terminal for a traceback. The most common cause is an invalid or missing API key.
  • If the response takes more than 30 seconds, the Gemini API might be rate-limited. Wait a minute and try again.

My Tailor feature is returning an error. Can you help me figure out what went wrong?

Test the Score feature

The Score feature uses Gemini's structured output to return numeric scores and actionable suggestions. This is where the Pydantic model you defined earlier forces the AI to respond in a consistent JSON format.

  • Keep the same resume and job description text in both text areas.
  • Click the Score Resume button.
  • Wait for the results to load.

You should see a grid of four score cards (Overall Score, Keyword Match, Impact & Metrics, Relevance), a summary paragraph, and a list of actionable suggestions with orange left borders.

Why are structured outputs useful here?

Without structured outputs, the AI might return scores in different formats each time ("75/100" vs "75 out of 100" vs "75%"). The ResumeScore Pydantic model forces the response into a predictable JSON shape with integer fields.

This means your frontend can always rely on score.overall_score being a number it can display directly, without any parsing or guesswork.

Scores not displaying correctly?

  • If you see raw JSON text instead of the score cards, check your browser's developer console for JavaScript errors.
  • If the scores appear but suggestions are missing, the AI may have returned an empty suggestions list. Try clicking Score Resume again.
  • If you get a 500 error in the terminal mentioning "ValidationError," the AI returned a response that didn't match the Pydantic schema. This is rare with structured outputs but can happen. Retry the request.

My Score feature is broken or returning unexpected results. What should I check?

Secret mission

Add a Cover Letter Generator

Your ResuMatch app can tailor and score resumes. But what about the cover letter? In this secret mission, you'll add a third AI-powered feature that generates a targeted cover letter draft based on your resume and the job description.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine and uses the free tier of the Gemini API. There are no ongoing cloud costs or paid subscriptions to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Flask development server (running in your terminal).
  • Project folder on your Desktop containing app.py, .env, and the static/ folder.
  • Python virtual environment (inside your project folder).
  • Gemini API key (in Google AI Studio).
  • Python 3.14 (installed on your system).

Keep everything running

No action needed. Choose this if you plan to keep using ResuMatch for real job applications.

  • Your Flask server stays running until you close the terminal or shut down your computer.
  • Your API key remains active on the Gemini free tier with no charges.
  • Python stays installed on your machine and is useful for other projects.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your project files stay on your machine for next time.

  • Switch to the terminal where Flask is running and press Ctrl+C to stop the server.
  • Your project folder, virtual environment, and API key all remain intact.
  • To resume later, activate your virtual environment and run python app.py again.

Delete - I don't want to use this again

Remove all project resources and clean up completely. Follow each step below.

  • Switch to the terminal where Flask is running and press Ctrl+C to stop the server.
  • Delete your project folder from the Desktop. Open a terminal and run the appropriate command for your operating system.

macOS

rm -rf ~/Desktop/[[PROJECT_FOLDER="your-project-folder-name"]]

Windows

rmdir /s /q "$env:USERPROFILE\Desktop\[[PROJECT_FOLDER="your-project-folder-name"]]"

This removes all project files including app.py, .env, the static/ folder, and the virtual environment.

  • Delete your Gemini API key. Open the Google Cloud API Credentials page at console.cloud.google.com/apis/credentials.
  • Find your API key in the list and click the Actions icon (three dots).
  • Select Delete API key, then confirm by clicking Delete in the modal.
  • Optionally, uninstall Python. Open Settings on Windows, go to Apps, find Python in the list, and select Uninstall. Only do this if you don't plan to use Python for anything else.

Nice Work!

Nice Work!

You just built ResuMatch, your own AI-powered resume optimization tool. Every time you apply for a job, you now have a personal assistant that tailors your resume, scores your fit, and drafts a cover letter.

You've learned how to:

  • Integrated the Google Gemini API using the official google-genai SDK to send prompts and receive AI-generated text.
  • Designed structured prompts and used Pydantic models with Gemini's structured output feature to get reliable JSON scores.
  • Built a full-stack Python Flask web application with an HTML/CSS/JS frontend that communicates with your API routes.
  • Secret Mission: Added a cover letter generator that creates targeted drafts from your resume and job description.

Ready to quiz yourself?