AI-Powered Job Posting Analyzer API
Use Google Gemini to extract structured data from raw job postings via FastAPI.
Introduction
30 Second Summary
Job postings are everywhere online, but they're a mess. Some are neatly structured with bullet points, others are dense paragraphs, and some barely tell you anything useful.
In this project, you will build a Python API that uses Google Gemini to transform raw job posting text into structured data. You'll serve it through a FastAPI endpoint with auto-generated interactive docs.
What You'll Build
You'll paste a raw job posting into a browser-based API playground, hit execute, and watch a clean JSON object appear with the title, company, skills, salary range, and experience level all neatly extracted by AI.
By the end of this project, you'll have:
- A data extraction pipeline powered by Google Gemini that transforms messy job posting text into structured JSON with fields like title, company, skills, and salary range.
- A REST API with auto-generated Swagger UI docs at /docs where you can paste any job posting and instantly see clean, structured results.
- Pydantic validation that catches unexpected AI output, flags low-confidence extractions, and handles edge cases like vague or incomplete postings gracefully.
- Secret Mission: Add a batch analysis endpoint that processes multiple job postings at once and returns aggregated insights like the most common skills across all postings.
Do I need API experience?
No API experience is needed. Basic Python knowledge (variables, functions, loops) is all you need to get started.
Before We Start
Before diving into code, take a moment to lock in what you're about to build and why it matters.
Set Up Your Python Environment and Get Your Gemini API Key
You know what you're building. Now it's time to set up the three tools that power your job posting analyzer.
Before writing any code, you need Python with the right version, the packages that connect to Gemini and serve your API, and a free API key from Google.
In this step, get ready to:
- Confirm Python 3.10+ is installed and create a project folder with a virtual environment.
- Install the google-genai SDK and FastAPI with all required dependencies.
- Get a free Gemini API key from Google AI Studio and set it as an environment variable.
Check your Python version and create a project folder
Your project uses the google-genai SDK, which requires Python 3.10 or higher. Let's confirm you have the right version before doing anything else.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by clicking Terminal in the top menu bar, then New Terminal.
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.10 or higher
You should see something like Python 3.12.4 (any version from 3.10 onward works). You're good to go.
ⓧ I see an older version
If you see a version below 3.10 (like 3.9 or 3.8), you need to install a newer version. On macOS, the system Python is often outdated.
- Go to python.org/downloads and download Python 3.12 or later.
- Run the installer and follow the prompts.
- Restart VS Code after the installation finishes.
- Open a new terminal and run the version check again to confirm you see 3.10 or higher.
macOS users with multiple Python versions
After installing a new Python on macOS, the system python3 may still point to the older version. Try running python3.12 --version (replacing 3.12 with the version you installed) to confirm the new version is available.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to python.org/downloads and download Python 3.12 or later.
- Run the installer and follow the prompts. On Windows, make sure to check the box that says Add Python to PATH.
- Restart VS Code after the installation finishes.
- Open a new terminal and run the version check again to confirm you see 3.10 or higher.
Now that Python is ready, let's create a project folder to hold all your code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir job-posting-analyzer
cd job-posting-analyzer
Your terminal prompt should now show job-posting-analyzer as the current directory.
A virtual environment is an isolated Python workspace. It keeps the packages you install for this project separate from any other Python work on your machine.
- Create and activate a virtual environment by running these commands:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This means the virtual environment is active.
Don't see (venv) in your prompt?
- Make sure you ran both the creation command and the activation command. The first creates the environment, the second switches into it.
- On Windows, if you get an error about execution policies, try running Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then retry the activate command.
- Why isn't my virtual environment activating?
Install the required packages
With your virtual environment active, you can install the two packages your project needs. These packages pull in everything else automatically.
- Install the Google Gemini SDK and FastAPI by running this command:
pip install google-genai "fastapi[standard]"
What did we just install?
This single command installs two packages and their dependencies:
- google-genai (v2.19.0) is the official Python SDK for calling the Gemini API. It handles authentication, request formatting, and response parsing.
- fastapi[standard] (v0.141.1) installs FastAPI along with Uvicorn (the web server) and Pydantic (for data validation). You get a full API stack in one install.
Before you run the next command, what do you think will print if the SDK installed correctly?
- Verify the SDK is installed by running this command:
python -c "from google import genai; print('SDK ready')"
You should see SDK ready printed in your terminal. This confirms the google-genai package installed correctly and Python can import it.
Seeing ModuleNotFoundError?
- Make sure your virtual environment is still active. You should see (venv) in your prompt. If not, rerun the activate command from the previous substep.
- Try running pip install google-genai again. If the install fails, check your internet connection.
- Help me fix my google-genai import error.
Get a free Gemini API key from Google AI Studio
Your code needs an API key to authenticate with the Gemini API. Google provides this for free through Google AI Studio. No billing account is required.
- Open your browser and go to aistudio.google.com.
- Sign in with your Google account. If you don't have one, create a free account first.
- Click Get API key in the left sidebar to navigate to the API keys page.
- Click Create API key.
- Copy the key that appears. You will not be shown this key again, so copy it now.
What is an API key?
An API key is a unique string that identifies your application to Google's servers. It controls who can make requests and tracks your usage. The Gemini API free tier includes free input and output tokens with no billing required.
New to Google AI Studio?
If this is your first time using Google AI Studio, it automatically creates a default project and API key after you accept the Terms of Service. You may already have a key ready to copy from the API keys page.
Now you need to make the key available to your Python code. An environment variable stores the key in your terminal session so your code can read it without hardcoding a secret into your files.
- Switch back to your VS Code terminal and set the environment variable by running this command:
macOS
export GEMINI_API_KEY=your-api-key-here
Windows
set GEMINI_API_KEY=your-api-key-here
What do I replace?
Replace your-api-key-here with the actual API key you just copied from Google AI Studio. Do not include quotes around the key value.
- Confirm the variable is set by running this command:
macOS
echo $GEMINI_API_KEY
Windows
echo %GEMINI_API_KEY%
You should see your API key printed back in the terminal.
Seeing a blank line or the literal text?
- If you see a blank line, the variable was not set. Re-run the export (macOS) or set (Windows) command, making sure there are no extra spaces around the = sign.
- If you see %GEMINI_API_KEY% printed literally on Windows, your terminal may be using PowerShell instead of Command Prompt. In PowerShell, set the variable with $env:GEMINI_API_KEY = "your-api-key-here" and check it with echo $env:GEMINI_API_KEY.
- Help me set my GEMINI_API_KEY environment variable.
Keep your API key safe
Never paste your API key directly into code files or share it publicly. Using an environment variable keeps the key out of your source code. If you close this terminal, you will need to set the variable again before running your project.
Your environment is ready. Python is installed, your packages are in place, and your API key is set. Next up, you'll write your first script that sends a job posting to Gemini and gets structured data back.
Build Your First Gemini-Powered Extraction Script
Your Python environment is ready and your Gemini API key is set. Now it is time to put that key to work.
Before building a full API, you need to confirm Gemini can actually extract structured data from messy job posting text. Starting with a simple Python script gives you a visible result in seconds.
In this step, get ready to:
- Send a job posting to Gemini and see its raw text response.
- Add a JSON schema and system prompt so Gemini returns clean, structured output.
Send a job posting to Gemini
The simplest way to test Gemini is to feed it a job posting and see what comes back. You will start with a bare-bones script that sends text and prints the raw response.
- Create a new file called extractor.py in your project folder by clicking File in the VS Code menu bar, then New File.
- Save it as extractor.py inside your project folder.
- Add the following code to create a function that sends text to Gemini and returns the raw response:
import os
from google import genai
def analyze_job_posting(text: str):
# Create a Gemini client using your API key
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# Send the job posting text to Gemini
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"Extract the job title, company, required skills, experience level, salary range, location, and employment type from this job posting:\n\n{text}",
)
return response.text
What does this code do?
- genai.Client(api_key=...) creates a client that authenticates with the Gemini API using the key you set earlier.
- client.models.generate_content() sends your prompt to the gemini-2.5-flash model and returns whatever Gemini generates.
- response.text holds the raw text that Gemini wrote back. Right now, that could be anything: a paragraph, a bulleted list, or something else entirely.
- Add this code below the function to give Gemini a realistic job posting to analyze:
if __name__ == "__main__":
sample_posting = """
Senior Python Developer at TechCorp Inc.
We are looking for a Senior Python Developer to join our backend team
in San Francisco, CA. You will work on building scalable APIs and
data pipelines.
Requirements:
- 5+ years of Python experience
- Experience with FastAPI or Django
- Knowledge of PostgreSQL and Redis
- Familiarity with Docker and Kubernetes
Salary: $150,000 - $180,000/year
Type: Full-time
"""
result = analyze_job_posting(sample_posting)
print(result)
What does this code do?
- if __name__ == "__main__": ensures this block only runs when you execute the file directly, not when another file imports from it.
- sample_posting is a realistic ~150-word job description with a title, requirements, salary, and location. This gives Gemini real data to work with.
Before you run this, what format do you think Gemini will return the data in? A table? A list? JSON?
- Save extractor.py and run it with this command:
python extractor.py
You should see Gemini's response printed in your terminal. It contains the right information, but it is plain, unstructured text. It might be a bulleted list, a paragraph, or some other freeform format.
Why is unstructured text a problem?
If you wanted to feed this response into another program, a database, or a front-end dashboard, you would need to parse that freeform text yourself. Every time Gemini changes its formatting, your parser breaks.
The fix is to tell Gemini exactly what shape the output should take, using a JSON schema. That way you always get a predictable, machine-readable response.
Seeing an error instead of a response?
- If you see a KeyError: 'GEMINI_API_KEY', your environment variable is not set. Return to Step 1 and re-run the set command in the same terminal session.
- If you see an authentication or permission error, double-check that you copied the full API key from Google AI Studio with no extra spaces.
- Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
Why is my extractor.py script failing when I run it?
Refine your prompt for structured JSON output
Gemini extracted the right information, but the format was unpredictable. You will now add two things: a system prompt that tells Gemini how to behave, and a JSON schema that forces a consistent output shape.
- In extractor.py, replace the imports at the top of the file with these updated imports:
import os
import json
from google import genai
from google.genai import types
- Below the imports, add this SYSTEM_PROMPT constant that tells Gemini exactly how to analyze job postings:
SYSTEM_PROMPT = """You are a job posting analyzer. Extract structured information from job posting text.
Return a JSON object with these fields:
- title: the job title (string or null)
- company: the company name (string or null)
- skills: list of required skills/technologies (list of strings, empty list if none found)
- experience_level: e.g. "entry", "mid", "senior", "lead" (string or null)
- salary_range: salary information as stated (string or null)
- location: job location (string or null)
- employment_type: e.g. "full-time", "part-time", "contract" (string or null)
- confidence: "high" if most fields are clearly stated, "medium" if some are ambiguous, "low" if very little info
Use null for any field you cannot determine. Do not guess or hallucinate information."""
What is a system prompt?
A system prompt sets the ground rules for how the LLM behaves. It runs before every user message. Here, it tells Gemini to act as a job posting analyzer, defines the exact fields to extract, and instructs it to use null instead of guessing when information is missing.
- Below SYSTEM_PROMPT, add this JOB_POSTING_SCHEMA dictionary that defines the exact JSON structure Gemini must follow:
JOB_POSTING_SCHEMA = {
"type": "object",
"properties": {
"title": {"type": ["string", "null"]},
"company": {"type": ["string", "null"]},
"skills": {"type": "array", "items": {"type": "string"}},
"experience_level": {"type": ["string", "null"]},
"salary_range": {"type": ["string", "null"]},
"location": {"type": ["string", "null"]},
"employment_type": {"type": ["string", "null"]},
"confidence": {"type": "string"},
},
"required": [
"title",
"company",
"skills",
"experience_level",
"salary_range",
"location",
"employment_type",
"confidence",
],
}
What does the JSON schema do?
The system prompt describes the fields in natural language. The schema enforces them structurally. When you pass this schema to the Gemini API, it guarantees the response is valid JSON matching this exact shape. No more guessing what format comes back.
Fields like title use ["string", "null"] to allow null values when information is missing. The skills field uses an array type so Gemini returns a list instead of a comma-separated string.
Now you need to update the function to use these new constants. The google-genai SDK lets you pass a GenerateContentConfig object that controls how Gemini responds.
- In extractor.py, find your existing analyze_job_posting function and replace it entirely with this updated version:
def analyze_job_posting(text: str):
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"Analyze this job posting:\n\n{text}",
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
response_mime_type="application/json",
response_json_schema=JOB_POSTING_SCHEMA,
temperature=0.1,
),
)
data = json.loads(response.text)
return data
What changed in the function?
- config=types.GenerateContentConfig(...) passes all your constraints to Gemini in one object.
- response_mime_type="application/json" tells Gemini to return JSON instead of freeform text.
- response_json_schema=JOB_POSTING_SCHEMA locks the JSON into the exact shape you defined.
- temperature=0.1 makes Gemini's output more deterministic. Lower temperature means less creativity and more consistency, which is exactly what you want for data extraction.
- json.loads(response.text) parses the JSON string into a Python dictionary you can work with programmatically.
- Finally, scroll down to the last line of your if __name__ block and find this line:
print(result)
- Replace it with this line, which formats the JSON output with readable indentation:
print(json.dumps(result, indent=2))
You have added a system prompt, a JSON schema, and a structured config to your function. What do you think the output looks like this time compared to the freeform text you saw earlier?
- Save extractor.py and run it again:
python extractor.py
You should see clean, structured JSON printed in your terminal. Every field you defined in the schema is present: title, company, skills, experience_level, salary_range, location, employment_type, and confidence.
Not seeing JSON output?
- If you see a NameError mentioning SYSTEM_PROMPT or JOB_POSTING_SCHEMA, make sure both constants are defined above the analyze_job_posting function, not below it.
- If you see a NameError mentioning types or json, check that you updated your imports at the top of the file to include import json and from google.genai import types.
- If the output still looks like freeform text, make sure you replaced the entire old function with the new version that includes the config parameter.
Help me debug why my extractor.py is not returning structured JSON.
✔️ Awesome, I've got everything!
Great. Double check you have saved extractor.py with all the changes.
ⓧ I'd like to double check the full code
Here is the complete extractor.py at this point. Your file should contain all of the following sections, in this order:
- Imports (os, json, genai, types) at the top.
- SYSTEM_PROMPT constant defining the analyzer instructions.
- JOB_POSTING_SCHEMA dictionary defining the JSON structure.
- analyze_job_posting() function with GenerateContentConfig including response_mime_type and response_json_schema.
- if __name__ == "__main__": block with sample_posting and json.dumps(result, indent=2).
Scroll through the code chunks above and confirm each section is present in your file, in order.
Your extraction script is working. Gemini takes in messy job posting text and returns clean, predictable JSON every time. Next up, you will add a safety net that catches bad data before it reaches your users.
Add Pydantic Validation to Catch Bad AI Output
In the last step, you got Gemini to return structured JSON from a raw job posting. That's a solid start. But right now, your script takes whatever Gemini sends back and passes it straight through with zero checks.
LLMs can hallucinate field names, return numbers where you expect strings, or skip fields entirely. Pydantic is a data validation library that catches these problems automatically. In this step, you'll define a strict data model for job postings and run every Gemini response through it before using the data.
In this step, get ready to:
- Define a JobPosting Pydantic model with typed fields for each piece of job data.
- Wire the model into your extraction function to validate every Gemini response.
- Test with both a detailed posting and a vague one to see validation in action.
Define the JobPosting Pydantic model
Every Gemini response currently comes back as a raw Python dictionary. Dictionaries don't enforce structure. If the AI returns a number for title or misspells a key, your code would silently accept it.
- In extractor.py, find the line that says from google.genai import types.
- Add the following code directly below it:
from pydantic import BaseModel, ValidationError
# Define the exact shape every Gemini response must match
class JobPosting(BaseModel):
title: str | None = None
company: str | None = None
skills: list[str] = []
experience_level: str | None = None
salary_range: str | None = None
location: str | None = None
employment_type: str | None = None
confidence: str = "low"
What does this code do?
BaseModel is Pydantic's base class. Any class that inherits from it gets automatic data validation. When you create a JobPosting from a dictionary, Pydantic checks that every value matches the declared type.
ValidationError is the exception Pydantic raises when data doesn't match. If Gemini returned a number where a string was expected, Pydantic would raise a ValidationError with a clear message telling you which field failed and why.
Fields like title: str | None = None accept either a string or null. This handles job postings where certain information is missing. The confidence field defaults to "low" so incomplete postings get flagged automatically.
- Save extractor.py.
Wire the model into your extraction function
The model exists, but it's not connected to your extraction logic yet. Three small edits wire everything together so every Gemini response passes through Pydantic before your code uses it.
- In extractor.py, scroll down to the analyze_job_posting function definition.
- Find this line:
def analyze_job_posting(text: str):
- Add a return type hint so the function declares it returns a JobPosting. Update the line to look like this:
def analyze_job_posting(text: str) -> JobPosting:
- Inside the same function, find the last line:
return data
- Replace it with the following, which passes the raw dictionary through Pydantic validation:
return JobPosting(**data)
- Now scroll down to the if __name__ == "__main__": block. Find the print line:
print(json.dumps(result, indent=2))
- Replace it with this. The .model_dump() method converts the Pydantic object back to a dictionary so json.dumps can serialize it:
print(json.dumps(result.model_dump(), indent=2))
- Save extractor.py.
- Run the script to confirm the model works with the existing sample posting by running this command:
python extractor.py
You should see the same structured JSON output as before. The output now passes through Pydantic validation before printing.
Seeing an AttributeError or TypeError?
- If you see an error mentioning model_dump, make sure you updated the print line to use result.model_dump() instead of passing result directly.
- If you see an error about JobPosting not being defined, check that the class definition is placed between the imports and the SYSTEM_PROMPT variable.
- If you see an error about return data still returning a dict, confirm you changed it to return JobPosting(**data).
Still stuck? Help me debug a Pydantic validation error in my extractor.py script.
Test validation with a vague job posting
The detailed sample posting made it easy for Gemini. Real-world data is messier. Let's see how your validated pipeline handles a posting that barely contains any information.
- In extractor.py, find the sample_posting variable inside the if __name__ == "__main__": block.
- Replace its entire value with this short, vague posting:
sample_posting = """Looking for a coder, apply now"""
- Save extractor.py.
Before you run this, what do you think the output will look like? Will Pydantic reject it, or will it still produce a valid result?
- Run the script again:
python extractor.py
You should see valid structured output with most fields set to null and a confidence of "low". Pydantic accepted the data because null is a valid value for str | None fields. The model didn't reject it. It validated it.
Why didn't Pydantic reject the vague posting?
Pydantic's job is to enforce data types, not data quality. If Gemini returns null for company, that's valid according to your model (str | None). If Gemini returned the number 42 for company, Pydantic would raise a ValidationError because an integer doesn't match str | None.
The confidence field is what tells your users about data quality. You'll use this in a later step to help API consumers decide whether to trust the result.
- Now restore the original sample posting. Press Cmd+Z (macOS) or Ctrl+Z (Windows) repeatedly until sample_posting is back to the full Senior Python Developer text.
- Save extractor.py.
✔️ Awesome, I've got everything!
Great. Confirm you've saved extractor.py with the original sample posting restored.
ⓧ I'd like to double check the full code
Your extractor.py should have these sections in order. Scroll up and confirm each one is present:
- Imports: os, json, genai, types, BaseModel, and ValidationError.
- The JobPosting class with 8 fields (title through confidence).
- SYSTEM_PROMPT and JOB_POSTING_SCHEMA unchanged from the previous step.
- The analyze_job_posting function with -> JobPosting return type and return JobPosting(**data) at the end.
- The if __name__ block with the original detailed sample posting and result.model_dump() in the print line.
Your extraction pipeline now validates every Gemini response through a strict Pydantic model. Next up, you'll wrap this logic in a FastAPI endpoint so anyone can send job postings to your API and get structured data back.
Serve It Through a FastAPI Endpoint
Your extraction script can parse job postings and return structured data. Right now, only you can use it by running extractor.py directly on your machine.
In this step, you'll wrap your extraction logic in a FastAPI REST API. FastAPI auto-generates interactive Swagger docs where anyone can test your endpoints live in the browser.
In this step, get ready to:
- Create a main.py file with a FastAPI app and data models.
- Add a health check endpoint and a job posting analysis endpoint.
- Start the server and test your API through interactive Swagger docs.
Create main.py with your FastAPI app and models
Your extractor.py already has the JobPosting model, the system prompt, and the JSON schema. Your new API file will import and reuse all of them.
- Create a new file called main.py in your project folder (the same folder where extractor.py lives).
- Paste the following code to set up the app and define your request and response shapes:
import os
import json
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
# Reuse the extraction logic from your script
from extractor import JobPosting, SYSTEM_PROMPT, JOB_POSTING_SCHEMA
# Create the FastAPI app with metadata for the docs page
app = FastAPI(
title="Job Posting Analyzer API",
description="AI-powered API that extracts structured data from job postings using Google Gemini",
version="1.0.0",
)
# Define the shape of incoming requests
class AnalyzeRequest(BaseModel):
text: str = Field(
...,
min_length=20,
description="Raw job posting text to analyze",
)
# Define the shape of outgoing responses
class AnalyzeResponse(BaseModel):
posting: JobPosting
raw_text_length: int
What does this code do?
The first block of imports pulls in FastAPI for the web framework, Pydantic for data validation, and the Gemini SDK. The from extractor import ... line reuses everything you already built instead of duplicating it.
The FastAPI() call creates your app instance. The title, description, and version values show up on the auto-generated docs page.
The AnalyzeRequest model defines what callers send to your API. The min_length=20 constraint rejects obviously too-short input. AnalyzeResponse wraps the validated JobPosting with a character count for context.
- Save main.py.
Add your API endpoints and start the server
Your app needs two endpoints. A quick health check proves the server is running. The analysis endpoint does the real work of parsing job postings through Gemini.
- In main.py, add the health check endpoint below the AnalyzeResponse class by pasting this code:
# Quick status check endpoint
@app.get("/health")
def health_check():
return {"status": "healthy", "model": "gemini-2.5-flash"}
What does this endpoint do?
The @app.get("/health") decorator tells FastAPI to handle GET requests at the /health path. The function returns a simple dictionary confirming the server is alive and which model it uses.
- Save main.py.
- Start the server by running this command:
uvicorn main:app --reload
What does this command do?
The uvicorn command starts a web server. main:app tells it to look in main.py for the variable called app. The --reload flag auto-restarts the server whenever you save changes to your code.
This command keeps your terminal busy. You will not be able to type other commands in this terminal while the server runs. Press Ctrl+C to stop it later.
You should see output ending with a line like Uvicorn running on http://127.0.0.1:8000 followed by Application startup complete.
- Open your browser and go to:
http://127.0.0.1:8000/health
You should see a JSON response: {"status":"healthy","model":"gemini-2.5-flash"}. Your API is live.
Server not starting?
- If you see ModuleNotFoundError: No module named 'fastapi', make sure your virtual environment is activated before running the command.
- If you see Error loading ASGI app. Could not import module "main", make sure you are running the command from the same folder where main.py lives.
- If your browser shows nothing at http://127.0.0.1:8000/health, double check that the terminal still shows the server running without errors.
Need more help? Why won't my FastAPI server start?
Add the analysis endpoint
The health check proves your server is alive. Now you need the endpoint that actually does the job posting analysis.
- In main.py, add the analysis endpoint below the health_check function by pasting this code:
@app.post("/analyze", response_model=AnalyzeResponse)
def analyze_posting(request: AnalyzeRequest):
# Create a Gemini client using the API key from environment
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
try:
# Send the job posting text to Gemini for analysis
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"Analyze this job posting:\n\n{request.text}",
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
response_mime_type="application/json",
response_json_schema=JOB_POSTING_SCHEMA,
temperature=0.1,
),
)
# Parse the JSON response and validate it with Pydantic
data = json.loads(response.text)
posting = JobPosting(**data)
return AnalyzeResponse(
posting=posting,
raw_text_length=len(request.text),
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
What does this endpoint do?
The @app.post("/analyze") decorator registers a POST endpoint. The response_model=AnalyzeResponse parameter tells FastAPI to validate and document the response shape automatically.
Inside the function, the code follows the same pattern as your extractor.py script. It creates a Gemini client, sends the request text for analysis, parses the JSON response, and validates it through JobPosting.
The try/except block catches any error (network issues, Gemini outages, invalid JSON) and converts it to a clean HTTP 500 response instead of crashing the server.
- Save main.py. The server auto-reloads because of the --reload flag.
Before you test, what do you think will happen when you open the Swagger docs page and send a job posting? Will you see raw text or structured JSON?
- Open your browser to:
http://127.0.0.1:8000/docs
You should see the Swagger UI page titled Job Posting Analyzer API with your two endpoints listed.
- Click on the POST /analyze endpoint to expand it.
- Click the Try it out button.
- Replace the default request body with this sample job posting:
{
"text": "Senior Python Developer at TechCorp Inc. We are looking for a Senior Python Developer to join our backend team in San Francisco, CA. Requirements: 5+ years of Python experience, FastAPI or Django, PostgreSQL and Redis. Salary: $150,000 - $180,000/year. Full-time."
}
- Click Execute.
You should see a 200 response with structured JSON containing the extracted title, company, skills, salary_range, and other fields from the posting.
Why are auto-generated docs so useful?
FastAPI reads your Pydantic models and route decorators to build a live, interactive API explorer. You did not write any documentation. The AnalyzeRequest model tells Swagger what fields to show, and the AnalyzeResponse model tells it what the response looks like.
In an interview, you can open /docs, paste a job posting, and demo your AI pipeline in real time. No Postman setup, no curl commands.
Seeing an error in the Swagger response?
- A 500 response usually means the Gemini API call failed. Check that your GEMINI_API_KEY environment variable is still set in the terminal where the server is running.
- A 422 response means your request body did not match the expected format. Make sure the JSON has a text field with at least 20 characters.
Still stuck? Help me debug my /analyze endpoint response
✔️ Awesome, I've got everything!
Great. Double check that you have saved main.py and that your server is still running.
ⓧ I'd like to double check the full code
Your main.py should contain three sections in this order. Scroll up and confirm each one is present:
- Imports, app creation, and the AnalyzeRequest / AnalyzeResponse models (the first code block in this step).
- The health_check function decorated with @app.get("/health") (the second code block).
- The analyze_posting function decorated with @app.post("/analyze") (the third code block).
Your extractor.py has not changed in this step.
Your API is live and serving structured job posting data through interactive docs. Next up, you'll handle the messy real-world inputs that could break it.
Handle Edge Cases and Add Error Responses
Your API is live and serving structured job posting data through an interactive Swagger UI. That's a working demo you could show in an interview right now.
But what happens when someone sends garbage? An empty string, a random sentence about the weather, or barely a few words. In this step, you'll put your API through its paces with three types of input to verify it handles each one gracefully.
In this step, get ready to:
- Test how your API rejects empty and too-short input with a 422 error.
- Test how your API responds to non-job-posting text with a low confidence score.
- Verify high-confidence results for a well-formatted job posting.
Test how your API handles invalid input
Your AnalyzeRequest model includes min_length=20 on the text field. This tells FastAPI to automatically reject any input shorter than 20 characters before it even reaches your Gemini extraction logic.
- If your server from the previous step is still running, skip to the next bullet. Otherwise, start it again by running:
uvicorn main:app --reload
Server won't start?
Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
Also confirm your GEMINI_API_KEY environment variable is still set. Environment variables reset when you close and reopen a terminal.
Why won't my FastAPI server start?
- Return to the Swagger UI at http://127.0.0.1:8000/docs in your browser.
- Click on the POST /analyze endpoint to expand it.
- Click Try it out.
- Replace the request body with this empty-string test:
{"text": ""}
- Click Execute.
You'll see a 422 response. FastAPI caught the empty string and rejected it before it ever reached Gemini.
- Now test with a short string. Replace the request body with:
{"text": "hiring now"}
- Click Execute.
Another 422. The text is only 10 characters, well under the 20-character minimum.
Why does FastAPI return 422?
A 422 status code means the server understood your request format, but the data inside failed validation. FastAPI uses 422 as its default response for any request that doesn't meet your Pydantic model's constraints.
Because you set min_length=20 on AnalyzeRequest, FastAPI automatically rejects short input and tells the caller exactly what went wrong in the error response body.
Test with non-job text and a real posting
Your API rejects input that's too short. But what about text that passes the length check yet isn't actually a job posting?
Before you test this, think about what you expect to happen. Will Gemini return an error? Will it make something up? Or will it handle it differently?
- Replace the request body with this non-job-posting text:
{"text": "The weather is really nice today and I enjoy spending my time outdoors in the sun"}
- Click Execute.
This time you'll see a 200 response. The text was long enough to pass validation, so FastAPI sent it to Gemini.
Look at the response body. The confidence field shows "low" and most other fields are null. Gemini recognized this isn't a job posting and responded honestly.
How does confidence scoring work?
Your SYSTEM_PROMPT in extractor.py explicitly instructs Gemini to rate its own confidence. If most fields are clearly stated, it returns "high". If some fields are ambiguous, it returns "medium". If there's very little usable information, it returns "low".
The prompt also tells Gemini to use null for any field it cannot determine. This prevents the model from guessing or hallucinating job details that don't exist in the input.
- Now test with a real job posting. Replace the request body with:
{"text": "Senior Python Developer at TechCorp Inc. We are looking for a Senior Python Developer to join our backend team in San Francisco, CA. Requirements: 5+ years Python, FastAPI, PostgreSQL, Docker, Kubernetes. Salary: $150,000 - $180,000/year. Full-time position."}
Before you click Execute, what do you expect the confidence score to be this time? What about the skills field?
- Click Execute.
You'll see a 200 response with "confidence": "high" and every field populated. The skills array contains each technology mentioned in the requirements.
Three inputs, three different responses. Your API rejects garbage, handles ambiguity honestly, and extracts rich data from real postings.
Seeing unexpected results?
If the confidence score or extracted fields don't match what you expected, remember that LLM responses can vary slightly between runs. The key things to check: confidence should be "high" for the real posting, and "low" for the weather text.
If you're getting 500 errors instead, check that your GEMINI_API_KEY environment variable is still set in the terminal where Uvicorn is running.
Why are my Gemini API results unexpected?
Secret mission
Add a Batch Analysis Endpoint
Your API handles one job posting at a time. Real data pipelines process records in bulk. In this mission, you'll add a batch endpoint that analyzes multiple job postings in a single request, computes cross-posting insights like the most common skills, and handles partial failures gracefully.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. The Gemini API free tier has rate limits but no charges.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder with extractor.py, main.py, and venv virtual environment.
- Gemini API key in your Google AI Studio account.
- FastAPI development server (if still running).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- Your code files stay on your local machine.
- The Gemini API free tier has no charges, so your API key costs nothing to keep.
- The GEMINI_API_KEY environment variable only lasts for your current terminal session. It disappears when you close the terminal.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your files and API key remain for when you return.
- Switch to the terminal where your FastAPI server is running and press Ctrl+C to stop it.
- When you come back, reactivate your virtual environment and start the server again by running:
uvicorn main:app --reload
- You will need to set the GEMINI_API_KEY environment variable again in your new terminal session before starting the server.
Delete - I don't want to use this again
Remove all project files and revoke your API key to start completely fresh.
- If the FastAPI server is still running, press Ctrl+C in the terminal to stop it.
- Deactivate the virtual environment by running:
deactivate
- Delete the project folder from your Desktop. On macOS or Linux, run:
cd ~/Desktop
rm -rf job-posting-analyzer
- On Windows, run:
cd ~\Desktop
rmdir /s /q job-posting-analyzer
- To revoke your Gemini API key, go to aistudio.google.com.
- Click Get API key in the left sidebar to open the API Keys page.
- Find your key in the list, click the trash icon next to it, and confirm the deletion.
Nice Work!
Nice Work!
You just built an AI-powered data extraction pipeline from scratch and served it through a production-style API. You can now paste any job posting into your API and instantly get clean, structured output powered by Google Gemini.
You've learned how to:
- Build a Python data extraction pipeline that sends unstructured text to Gemini and gets back structured JSON using prompt engineering and JSON schema constraints.
- Create a FastAPI REST API with auto-generated interactive Swagger docs at /docs for live testing and interview demos.
- Implement Pydantic data validation to catch unexpected AI output, handle edge cases with input validation and confidence scoring, and return clear error responses.
- Secret Mission: Added a batch processing endpoint that analyzes multiple job postings at once and returns aggregated insights like most common skills and experience levels.
Ready to quiz yourself?