Build an AI Code Review API with FastAPI

Build a REST API that reviews code snippets using AI via OpenRouter.

Introduction

30 Second Summary

Code reviews are one of the most valuable parts of software development, but getting feedback can be slow when teammates are busy. What if you could get an instant, structured review of any code snippet by hitting a single API endpoint?

In this project, you will build a REST API that accepts code snippets and returns AI-generated reviews with categorized issues, severity levels, and fix suggestions. You'll use FastAPI for the backend and OpenRouter to access a free large language model.

What You'll Build

You paste a code snippet into your browser, hit send, and get back a structured JSON review identifying bugs, style issues, and performance problems with concrete fix suggestions for each.

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

  • A working POST endpoint that accepts any code snippet and returns a structured review with issues categorized by severity and paired with concrete suggestions.
  • Built-in input validation that rejects bad requests with clean error messages, and error handling that gracefully catches AI failures instead of crashing.
  • Auto-generated interactive API docs at /docs where you can test every endpoint directly in the browser.
  • Secret Mission: A /compare endpoint that reviews the same logic implemented in two different languages side-by-side.

Are there any prerequisites?

You should be comfortable with Python fundamentals (variables, functions, imports) and have a basic understanding of APIs and HTTP methods (GET, POST). No prior FastAPI experience is needed.

Before We Start

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

You're building a Code Review API that accepts any code snippet and returns a structured, AI-generated review. Take a moment to think about how the pieces fit together: FastAPI handles routing and validation, Pydantic models shape your requests and responses, and OpenRouter provides free LLM access to power the actual review.

Set Up Your Project and Install Dependencies

Every Python project starts with a clean workspace. You need an isolated folder, a virtual environment to keep your packages separate from the rest of your system, and the right libraries installed before writing any code.

In this step, you will create a project folder, install FastAPI and the OpenRouter SDK, then spin up a minimal server to prove everything works.

In this step, get ready to:
  • Create your project folder and virtual environment with dependencies installed.
  • Write a minimal FastAPI app and configure your API key.
  • Run the dev server and confirm it responds in the browser.
Create your project folder and virtual environment

You will create a dedicated folder on your Desktop so it is easy to find later. Then you will set up a Python virtual environment inside it to keep this project's packages isolated.

  • 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.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir code-review-api
cd code-review-api
  • Confirm the folder exists by running:
ls

You should see an empty output since the folder is brand new.

  • Open this folder in VS Code by running:
code .

code . not recognized?

On macOS, you need to install the shell command first. Open VS Code manually (Cmd+Space, type Visual Studio Code), then press Cmd+Shift+P and run Shell Command: Install 'code' command in PATH.

On Windows, restart your Command Prompt after installing VS Code. If it still fails, open VS Code manually (Windows key, type Visual Studio Code), then click File > Open Folder and select code-review-api on your Desktop.

help me open my project folder in VS Code

VS Code opens with the code-review-api folder as your workspace. Now you will use VS Code's built-in terminal for the rest of this step.

  • Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
  • Create a virtual environment by running this command:

macOS

python3 -m venv venv

Windows

py -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

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

What is a virtual environment?

A virtual environment is an isolated copy of Python that keeps this project's packages separate from your system-wide Python installation. Any packages you install while the venv is active only exist inside this project's venv folder.

  • Install the project dependencies by running:
pip install "fastapi[standard]" openrouter python-dotenv

What are these packages?

  • fastapi[standard] installs FastAPI plus Uvicorn (the dev server) and other useful extras in one go.
  • openrouter is the official Python SDK for calling AI models through OpenRouter.
  • python-dotenv loads environment variables from a .env file so you can keep secrets out of your code.

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

Install failed or pip not found?

Make sure you see (venv) in your prompt. If not, re-run the activate command above.

On macOS, if pip is not found, try pip3 instead.

help me fix my pip install error

Create the starter files

With packages installed, you need two files: a main.py that defines your API, and a .env that stores your OpenRouter API key safely.

  • In the VS Code Explorer sidebar (left panel), click the New File icon.
  • Name the file main.py and press Enter.
  • Paste the following code into main.py:
from fastapi import FastAPI

# Create the FastAPI application instance
app = FastAPI(
    title="AI Code Review API",
    description="An API that reviews code snippets using AI via OpenRouter",
    version="1.0.0",
)


# Root endpoint that confirms the API is running
@app.get("/")
def root():
    return {"message": "AI Code Review API", "docs": "/docs"}

What does this code do?

  • FastAPI() creates your API application. The title, description, and version fields show up in the auto-generated docs.
  • @app.get("/") registers a GET route at the root URL. When someone visits your API's base address, they get back the welcome JSON.
  • Save main.py with Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you have saved your file, then move on to create the environment file below.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI(
    title="AI Code Review API",
    description="An API that reviews code snippets using AI via OpenRouter",
    version="1.0.0",
)


@app.get("/")
def root():
    return {"message": "AI Code Review API", "docs": "/docs"}
  • Click the New File icon again in the Explorer sidebar.
  • Name this file .env and press Enter.
  • Paste the following line into .env:
OPENROUTER_API_KEY=your-api-key-here
  • Replace your-api-key-here with your actual OpenRouter API key.
  • Save .env.

Why a .env file?

Storing your API key in a .env file keeps it out of your source code. If you ever push this project to GitHub, you would add .env to your .gitignore so the key never gets shared publicly.

✔️ Awesome, I've got everything!

Great. Make sure you replaced the placeholder with your real API key, then move on.

ⓧ I'd like to double check the full code

Your .env file should have exactly one line, in this format:

OPENROUTER_API_KEY=your-api-key-here

The value after the = sign should be your actual API key, not the placeholder text.

Run the server and verify

Everything is in place. Time to start the dev server and see your API respond in the browser.

  • In your VS Code terminal (make sure you still see (venv) in the prompt), start the server by running:
uvicorn main:app --reload

What does this command do?

  • uvicorn is the ASGI server that runs your FastAPI app. It was installed automatically as part of fastapi[standard].
  • main:app tells Uvicorn to look in main.py for a variable called app.
  • --reload makes the server restart automatically whenever you save a file. This keeps your terminal occupied. You will leave it running.

You should see output including a line like Uvicorn running on http://127.0.0.1:8000. The server is now running and waiting for requests.

Before you open the browser, what do you think you will see when you visit that URL? A blank page, an error, or something else?

  • Open your browser and navigate to http://127.0.0.1:8000.

You should see a JSON response:

{"message": "AI Code Review API", "docs": "/docs"}

That is your API responding. FastAPI also auto-generates interactive documentation for every endpoint you create.

  • Navigate to http://127.0.0.1:8000/docs in the same browser.

You should see a Swagger UI page titled AI Code Review API with your single GET / endpoint listed. This is where you will test all your endpoints throughout the project.

Browser shows connection refused?

Check your terminal for error messages. If you see ModuleNotFoundError, make sure (venv) is in your prompt and re-run the pip install command.

If the terminal says the address is already in use, another process is on port 8000. Stop it or run uvicorn main:app --reload --port 8001 instead.

help me get my FastAPI server running

Your development environment is ready and your server is responding. Next up, you will build a POST endpoint that accepts code snippets and returns a structured review.

Build the Review Endpoint with a Hardcoded Response

Your server is running and responding to GET requests at the root path. Now you need the core feature: a POST endpoint that accepts code snippets and returns a structured review.

Connecting to the AI right away would make debugging harder if something breaks. Instead, you'll build the endpoint with hardcoded data first to prove your routing, request validation, and response shape all work correctly.

In this step, get ready to:
  • Define Pydantic models for request and response validation.
  • Create a POST /review endpoint with a hardcoded response.
  • Test the full request/response cycle in the interactive docs.
Define Pydantic models for the request and response

Every FastAPI endpoint needs a clear contract: what data comes in, and what data goes out. Pydantic models define that contract as Python classes, and FastAPI uses them to validate requests automatically and generate documentation.

  • Create a new file called models.py in your code-review-api folder.
  • Add the imports, the supported language enum, and the request model by pasting this code:
from pydantic import BaseModel, Field
from enum import Enum


# Defines which languages the API accepts
class SupportedLanguage(str, Enum):
    python = "python"
    javascript = "javascript"
    typescript = "typescript"
    java = "java"
    go = "go"
    rust = "rust"
    cpp = "cpp"
    csharp = "csharp"


# The shape of data the client sends to the API
class ReviewRequest(BaseModel):
    code: str = Field(..., min_length=1, max_length=5000, description="The code snippet to review")
    language: SupportedLanguage = Field(default=SupportedLanguage.python, description="Programming language of the snippet")

What does this code do?

SupportedLanguage is a string enum that limits which programming languages the API accepts. If a client sends "ruby", FastAPI will reject the request automatically.

ReviewRequest defines the input shape: a code string (1 to 5000 characters) and a language that defaults to Python if not specified. The Field(...) with ... means the field is required.

  • Now add the response models below the ReviewRequest class in the same file:
# A single issue found during the review
class Issue(BaseModel):
    description: str = Field(..., description="What the issue is")
    severity: str = Field(..., description="low, medium, or high")
    line_number: int | None = Field(default=None, description="Approximate line number")
    suggestion: str = Field(..., description="How to fix it")


# The complete response returned to the client
class ReviewResponse(BaseModel):
    issues: list[Issue] = Field(default_factory=list, description="List of issues found")
    summary: str = Field(..., description="Overall assessment of the code")
    model_used: str = Field(..., description="Which AI model performed the review")

What does this code do?

Issue represents a single code problem: what's wrong, how severe it is, where it is, and how to fix it. The line_number field is optional (int | None) because the AI might not always pinpoint an exact line.

ReviewResponse wraps everything together: a list of issues, an overall summary, and which model performed the review. This is exactly what the client receives as JSON when they call the endpoint.

  • Save models.py.
  • Verify the models parse correctly by running this in your terminal:
python -c "from models import ReviewRequest, Issue, ReviewResponse; print('Models loaded successfully')"

You should see Models loaded successfully printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure your terminal is in the code-review-api folder where models.py lives. Also check that your virtual environment is still active (you should see (venv) at the start of your terminal prompt).

help me fix a ModuleNotFoundError when importing my models

Create the /review POST endpoint

With your data shapes defined, you can wire them into a real endpoint. The endpoint will accept a ReviewRequest body and return a hardcoded ReviewResponse so you can confirm the full data flow works.

  • In main.py, find this import line at the top:
from fastapi import FastAPI
  • Add the models import directly below it, so your imports section looks like this:
from fastapi import FastAPI

from models import (
    Issue,
    ReviewRequest,
    ReviewResponse,
)
  • Now scroll to the bottom of main.py, below the existing root() function.
  • Add the POST endpoint by pasting this code:
# POST endpoint that accepts code and returns a structured review
@app.post("/review", response_model=ReviewResponse)
def review_code(request: ReviewRequest):
    return ReviewResponse(
        issues=[
            Issue(
                description="Variable is not used",
                severity="low",
                line_number=2,
                suggestion="Remove the unused variable or use it somewhere.",
            ),
            Issue(
                description="Missing type annotations",
                severity="medium",
                line_number=1,
                suggestion="Add type hints to function parameters and return values.",
            ),
        ],
        summary="The code has minor issues but is generally readable.",
        model_used="hardcoded",
    )

What does this code do?

The @app.post("/review") decorator creates a POST route at /review. The response_model=ReviewResponse parameter tells FastAPI to validate the output and document the response shape in the Swagger UI.

The request: ReviewRequest parameter means FastAPI will parse the incoming JSON body into a ReviewRequest object automatically. If the body is missing code or sends an unsupported language, FastAPI returns a 422 error without your code running at all.

  • Save main.py.

Because the server is running with --reload, it automatically picks up your changes. Check your terminal where uvicorn is running. You should see a reload message confirming the server restarted.

Seeing an ImportError in the terminal?

If uvicorn shows an error like cannot import name 'Issue' from 'models', double-check that you saved models.py and that it contains both the Issue and ReviewResponse classes.

help me fix an ImportError when uvicorn reloads my FastAPI app

Test in the interactive docs

FastAPI auto-generates interactive API docs from your Pydantic models. You can test the endpoint directly in the browser without writing any client code.

  • Open your browser to http://127.0.0.1:8000/docs.
  • You should see the POST /review endpoint listed alongside your existing GET / route.
  • Click on the POST /review row to expand it.
  • Click the Try it out button on the right side.
  • Replace the example JSON in the request body with this:
{
  "code": "def hello():\n    x = 1\n    print('hi')",
  "language": "python"
}
  • Before you click Execute, predict: will the response contain the code you just sent, or something else entirely?
  • Click Execute.

You should see a 200 response with a JSON body containing two issues, a summary, and "model_used": "hardcoded". The response comes back the same regardless of what code you sent. That's expected. The AI integration comes in the next step.

Why build with hardcoded data first?

If something breaks after you wire up the AI, you'll know the bug is in the AI integration, not in your routing or data models. This pattern of building in layers (shape first, logic second) is standard practice in production API development.

Not seeing the POST /review endpoint in /docs?

Make sure you saved both main.py and models.py. Check your uvicorn terminal for any error messages. If the server crashed, fix the error and it will auto-restart.

help me figure out why my POST /review endpoint is not showing in FastAPI docs

✔️ Awesome, I've got everything!

Great. Double check you've saved both files.

ⓧ I'd like to double check the full code

Your project now has two files with code. Scroll up and confirm each section is present in order:

  • models.py: imports, SupportedLanguage enum (8 values), ReviewRequest model (code + language fields), Issue model (4 fields), ReviewResponse model (3 fields).
  • main.py: from fastapi import FastAPI import, from models import (Issue, ReviewRequest, ReviewResponse,) import, the app setup, the root() GET route, and the review_code() POST route returning a hardcoded ReviewResponse with two issues.

Your endpoint shape is locked in and validated. Next up, you'll replace that hardcoded response with a real AI-powered code review from OpenRouter.

Wire Up OpenRouter for Real AI Reviews

Your API accepts code snippets and returns properly structured JSON reviews. The routing works. The Pydantic models validate correctly. But the reviews are fake.

In this step, you'll connect to OpenRouter's free AI models to generate real code reviews. You'll build an AI service module that sends code to a language model and parses the structured response back into your Pydantic models.

In this step, get ready to:
  • Create an AI service module that connects to OpenRouter and returns structured reviews.
  • Design a system prompt that instructs the AI to return valid JSON matching your response schema.
  • Wire your existing endpoint to use real AI analysis instead of hardcoded data.
Create the AI service module

Separating AI logic into its own module keeps main.py focused on routing. The new ai_service.py file handles everything related to calling OpenRouter.

  • Create a new file called ai_service.py in your code-review-api folder and add the following imports and configuration:
import json
import os
import re

from dotenv import load_dotenv
from openrouter import OpenRouter

from models import Issue, ReviewResponse

load_dotenv()

API_KEY = os.getenv("OPENROUTER_API_KEY")
MODEL_ID = "meta-llama/llama-3.3-70b-instruct:free"

What does this code do?

  • load_dotenv() reads your .env file and loads OPENROUTER_API_KEY into the environment so os.getenv can find it.
  • MODEL_ID pins your requests to a specific free model instead of the random openrouter/free router. This ensures consistent output formatting.
  • The json and re imports are for parsing AI responses later in this file.
  • Save ai_service.py.
  • Confirm the file exists by running:
ls ai_service.py

You should see ai_service.py listed in the output.

File not found?

  • Make sure you created the file inside the code-review-api folder, not a subfolder.
  • Check that your terminal is in the same folder as main.py.

Why isn't my ai_service.py file showing up?

Craft the system prompt

The system prompt tells the AI exactly what format to return. Without precise instructions, the model might return prose, markdown, or inconsistently structured JSON.

  • In ai_service.py, add the following SYSTEM_PROMPT constant below the MODEL_ID line:
SYSTEM_PROMPT = """You are a senior software engineer performing a code review.
Analyze the provided code and return your review as valid JSON with this exact structure:
{
  "issues": [
    {
      "description": "Brief description of the issue",
      "severity": "low|medium|high",
      "line_number": 1,
      "suggestion": "How to fix it"
    }
  ],
  "summary": "Overall assessment of the code quality"
}

Rules:
- Only return valid JSON, no markdown fences, no extra text
- severity must be exactly "low", "medium", or "high"
- line_number should be approximate, use null if uncertain
- Be specific and actionable in suggestions
- If the code is clean, return an empty issues array with a positive summary
"""

Why be so specific in the prompt?

LLMs follow instructions better when you show them the exact JSON structure you expect. Including field names, value constraints, and example output reduces the chance of getting malformed responses.

The "no markdown fences" rule is critical. Many models default to wrapping JSON in ```json blocks, which breaks json.loads() parsing. You'll add a fallback parser next as a safety net.

  • Save ai_service.py.

The prompt is stored but not used yet. The next chunk adds the function that sends it to OpenRouter.

  • Still in ai_service.py, add the extract_json helper function below the SYSTEM_PROMPT:
def extract_json(text: str) -> dict | None:
    """Extract JSON from AI response, handling markdown fences and extra text."""
    text = text.strip()

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    fence_pattern = re.compile(r"```(?:json)?\s*\n?(.*?)\n?```", re.DOTALL)
    match = fence_pattern.search(text)
    if match:
        try:
            return json.loads(match.group(1).strip())
        except json.JSONDecodeError:
            pass

    start = text.find("{")
    end = text.rfind("}")
    if start != -1 and end != -1 and end > start:
        try:
            return json.loads(text[start:end + 1])
        except json.JSONDecodeError:
            pass

    return None

Why do you need a JSON extraction helper?

Even when instructed to return only JSON, models sometimes wrap output in markdown code fences or add commentary before the JSON object. This function tries three extraction strategies in order:

  • Direct parse (the ideal case where the model followed instructions perfectly).
  • Regex extraction from markdown fences (the most common failure mode).
  • Brace-matching to find the outermost JSON object (the last resort).
  • Below extract_json, add the first part of the analyze_code function. This sends the code to OpenRouter and handles parsing failures:
def analyze_code(code: str, language: str) -> ReviewResponse:
    """Send code to OpenRouter for AI review and return structured response."""
    user_message = f"Review this {language} code:\n\n```{language}\n{code}\n```"

    with OpenRouter(api_key=API_KEY) as client:
        response = client.chat.send(
            model=MODEL_ID,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": user_message},
            ],
            stream=False,
        )

    ai_text = response.choices[0].message.content
    parsed = extract_json(ai_text)

    if parsed is None:
        return ReviewResponse(
            issues=[],
            summary="The AI returned a response that could not be parsed. Please try again.",
            model_used=MODEL_ID,
        )

What does this code do?

  • The with OpenRouter(api_key=API_KEY) as client pattern is the SDK's context manager for synchronous usage. It handles connection cleanup automatically.
  • client.chat.send() sends the system prompt and user message to the pinned model, with stream=False to get the full response at once.
  • If extract_json returns None, the function returns a safe fallback response instead of crashing.
  • Directly below the if parsed is None block (still inside analyze_code), add the code that builds the final response from the parsed JSON:
    issues = []
    for item in parsed.get("issues", []):
        issues.append(Issue(
            description=item.get("description", "Unknown issue"),
            severity=item.get("severity", "medium"),
            line_number=item.get("line_number"),
            suggestion=item.get("suggestion", "No suggestion provided"),
        ))

    return ReviewResponse(
        issues=issues,
        summary=parsed.get("summary", "Review complete."),
        model_used=MODEL_ID,
    )

Why use .get() with defaults?

The AI might omit a field or return it in an unexpected format. Using .get("key", default) ensures your code never crashes on a missing field. Each issue gets safe fallback values if the AI skips a property.

  • Save ai_service.py. The module is complete but not connected to your API yet. The next substep wires it in.

✔️ Awesome, I've got everything!

Great. Double check you've saved ai_service.py and move on to connect it to your endpoint.

ⓧ I'd like to double check the full code

Here's the complete ai_service.py file. Scroll up and confirm you have each section in this order: imports, load_dotenv() and config, SYSTEM_PROMPT, extract_json function, and analyze_code function. Each chunk you added above appears in this order in the final file.

Connect the endpoint to the AI service

The AI service module is ready. Now you need to replace the hardcoded response in main.py with a call to analyze_code.

  • Replace the entire contents of main.py with the following updated version:
from fastapi import FastAPI

from ai_service import analyze_code
from models import (
    Issue,
    ReviewRequest,
    ReviewResponse,
)

app = FastAPI(
    title="AI Code Review API",
    description="An API that reviews code snippets using AI via OpenRouter",
    version="1.0.0",
)


@app.get("/")
def root():
    return {"message": "AI Code Review API", "docs": "/docs"}


@app.post("/review", response_model=ReviewResponse)
def review_code(request: ReviewRequest):
    result = analyze_code(request.code, request.language.value)
    return result

What changed?

  • A new import line from ai_service import analyze_code brings in your AI function.
  • The review_code function body is now two lines: call analyze_code with the request data, then return the result.
  • request.language.value extracts the string value from the SupportedLanguage enum (e.g. "python") so the AI prompt reads naturally.
  • Save main.py. The dev server auto-reloads.

Before you test, think about this: if you POST a code snippet with an obvious bug (like using = instead of == in a condition), do you expect the AI to catch it?

  • Open http://127.0.0.1:8000/docs in your browser.
  • Click the POST /review endpoint, then click Try it out.
  • Replace the request body with this code snippet that has a bug:
{
  "code": "def check_admin(user_role):\n    if user_role = 'admin':\n        return True\n    return False",
  "language": "python"
}
  • Click Execute.

The response takes a few seconds as the AI processes your code. You should see a JSON response with an issues array that identifies the = vs == bug, a summary field, and model_used showing meta-llama/llama-3.3-70b-instruct:free.

Seeing a 500 Internal Server Error?

  • Check your terminal for the full error traceback. The most common cause is a missing or invalid OPENROUTER_API_KEY in your .env file.
  • Make sure your .env file is in the same folder as main.py and contains OPENROUTER_API_KEY= followed by your actual key (no quotes around the value).
  • If you see an import error mentioning openrouter, confirm your virtual environment is active (you should see (venv) in your terminal prompt).

Help me debug my 500 error when calling the /review endpoint

Your API is now returning real AI-powered code reviews. Next up, you'll add proper input validation and error handling so the API fails gracefully instead of crashing when something goes wrong.

Add Structured Validation and Error Handling

Your API can review code, but what happens when things go wrong? If the AI service is down, the API key is invalid, or the learner sends garbage input, your endpoint will crash with an ugly stack trace.

Production APIs handle failures gracefully. In this step, you will see your API crash, then add error handling that returns clean, helpful error messages instead.

In this step, get ready to:
  • Confirm that input validation already catches bad requests with a 422 error.
  • Add error handling for AI service failures so the API returns clean 503 responses.
  • Test the full error handling flow end-to-end.
Confirm your input validation works

Your ReviewRequest model already enforces constraints with Pydantic. The code field requires at least 1 character and allows at most 5000. FastAPI automatically rejects requests that violate these rules.

  • Switch back to your browser at http://127.0.0.1:8000/docs.
  • Click on the POST /review endpoint, then click Try it out.
  • Replace the request body with this empty-code payload:
{
  "code": "",
  "language": "python"
}
  • Click Execute.

You should see a 422 response with a detail array explaining that the code field failed the minimum length check. That is Pydantic validating the input for you automatically.

Why 422 and not 400?

FastAPI uses HTTP 422 (Unprocessable Entity) for validation errors by convention. It means the server understood the request format but the data inside did not meet the rules. The response body includes the field name, the error message, and the error type so API consumers know exactly what to fix.

See your API crash without error handling

Input validation handles bad data from clients. But what if the AI service itself fails? Right now your endpoint has no protection against that.

  • Return to VS Code and open .env.
  • Change your OPENROUTER_API_KEY value to invalid-key-test temporarily.
  • Save .env.

Before you test this, predict what will happen. Will the API return a helpful error message, or will something else go wrong?

  • Switch back to /docs in your browser.
  • Send a POST to /review with any valid code snippet (e.g. print('hello')).
  • Click Execute.

You will see a 500 Internal Server Error in the Swagger response. If you check your terminal, you will see a stack trace with the raw exception. This is the exact problem. Your API consumers see no useful information about what went wrong.

Why is this a problem?

A 500 error tells the client nothing. They cannot tell whether their request was wrong, whether the AI service is down, or whether they exceeded a rate limit. Production APIs translate internal failures into specific, actionable error codes and messages.

Add error handling to your endpoint

You need two things: a response model for errors (so the Swagger docs show the error schema) and a try/except block that catches failures and returns meaningful HTTP status codes.

  • In VS Code, open models.py and add this class at the end of the file:
class ErrorResponse(BaseModel):
    detail: str

What does this model do?

The ErrorResponse model tells FastAPI what your error JSON looks like. This means the auto-generated docs at /docs will show API consumers the exact shape of error responses, not just the success case.

  • Save models.py.

Now update main.py to use HTTPException and the new ErrorResponse model.

  • In main.py, find this import line at the top:
from fastapi import FastAPI
  • Replace it with:
from fastapi import FastAPI, HTTPException
  • Find the models import block:
from models import (
    Issue,
    ReviewRequest,
    ReviewResponse,
)
  • Replace it with this updated import that adds ErrorResponse:
from models import (
    ErrorResponse,
    Issue,
    ReviewRequest,
    ReviewResponse,
)
  • Now find the /review endpoint function:
@app.post("/review", response_model=ReviewResponse)
def review_code(request: ReviewRequest):
    result = analyze_code(request.code, request.language.value)
    return result
  • Replace the entire endpoint with this version that adds error handling:
@app.post("/review", response_model=ReviewResponse, responses={503: {"model": ErrorResponse}})
def review_code(request: ReviewRequest):
    try:
        result = analyze_code(request.code, request.language.value)
        return result
    except Exception as e:
        error_message = str(e)
        if "401" in error_message or "unauthorized" in error_message.lower():
            raise HTTPException(status_code=503, detail="AI service authentication failed. Check your API key.")
        if "429" in error_message:
            raise HTTPException(status_code=503, detail="AI service rate limit reached. Try again in a minute.")
        raise HTTPException(status_code=503, detail=f"AI service unavailable: {error_message}")

What does this error handling do?

  • The try/except wraps the AI call so any failure is caught instead of crashing the server.
  • Checking for 401 or unauthorized in the error identifies authentication failures. The response tells the consumer to check their key.
  • Checking for 429 identifies rate limiting. The free tier allows 20 requests per minute.
  • The responses={503: {"model": ErrorResponse}} parameter documents the error schema in your Swagger UI.
  • All AI failures map to 503 Service Unavailable because the problem is with the upstream AI service, not with the client's request.
  • Save main.py. Uvicorn will auto-reload.

Seeing an import error in the terminal?

Make sure you added ErrorResponse to models.py and saved that file before saving main.py. Uvicorn reloads when main.py changes, but it imports from models.py at load time.

help me fix an import error in my FastAPI app

Verify your error handling works

Your .env still has the invalid key from earlier. This is the perfect setup to test error handling.

  • Switch back to /docs in your browser.
  • Send a POST to /review with the code print('hello') and click Execute.

This time, instead of a 500 crash, you should see a 503 response with a clear message about the AI service being unavailable. Your terminal stays clean with no stack trace.

  • Now fix your API key. Open .env and change OPENROUTER_API_KEY back to your real key.
  • Save .env.
  • In /docs, test the empty-code payload again to confirm the 422 validation error still works:
{
  "code": "",
  "language": "python"
}
  • Click Execute. You should see a 422 response listing the validation error for the code field.

Still seeing a 500 error instead of 503?

Check that your main.py saved correctly. Look at the terminal to confirm Uvicorn reloaded without errors. The reload message looks like WARNING: ... Detected changes in 'main.py'. Reloading... followed by Application startup complete.

help me debug why my FastAPI error handling is not catching exceptions

✔️ Awesome, I've got everything!

Great. Double check you have saved both models.py and main.py.

ⓧ I'd like to double check the full code

Here is the full models.py at this point:

from pydantic import BaseModel, Field
from enum import Enum


class SupportedLanguage(str, Enum):
    python = "python"
    javascript = "javascript"
    typescript = "typescript"
    java = "java"
    go = "go"
    rust = "rust"
    cpp = "cpp"
    csharp = "csharp"


class ReviewRequest(BaseModel):
    code: str = Field(..., min_length=1, max_length=5000, description="The code snippet to review")
    language: SupportedLanguage = Field(default=SupportedLanguage.python, description="Programming language of the snippet")

And the rest of models.py:

class Issue(BaseModel):
    description: str = Field(..., description="What the issue is")
    severity: str = Field(..., description="low, medium, or high")
    line_number: int | None = Field(default=None, description="Approximate line number")
    suggestion: str = Field(..., description="How to fix it")


class ReviewResponse(BaseModel):
    issues: list[Issue] = Field(default_factory=list, description="List of issues found")
    summary: str = Field(..., description="Overall assessment of the code")
    model_used: str = Field(..., description="Which AI model performed the review")


class ErrorResponse(BaseModel):
    detail: str

Here is the full main.py:

from fastapi import FastAPI, HTTPException

from ai_service import analyze_code
from models import (
    ErrorResponse,
    Issue,
    ReviewRequest,
    ReviewResponse,
)

app = FastAPI(
    title="AI Code Review API",
    description="An API that reviews code snippets using AI via OpenRouter",
    version="1.0.0",
)

And the endpoints in main.py:

@app.get("/")
def root():
    return {"message": "AI Code Review API", "docs": "/docs"}


@app.post("/review", response_model=ReviewResponse, responses={503: {"model": ErrorResponse}})
def review_code(request: ReviewRequest):
    try:
        result = analyze_code(request.code, request.language.value)
        return result
    except Exception as e:
        error_message = str(e)
        if "401" in error_message or "unauthorized" in error_message.lower():
            raise HTTPException(status_code=503, detail="AI service authentication failed. Check your API key.")
        if "429" in error_message:
            raise HTTPException(status_code=503, detail="AI service rate limit reached. Try again in a minute.")
        raise HTTPException(status_code=503, detail=f"AI service unavailable: {error_message}")

Your API now handles both bad input and service failures gracefully. Next up, you will add health check and model info endpoints to complete your production API.

Add a Health Check and Model Info Endpoint

Your API now handles real code reviews with graceful error handling. The next step is adding endpoints that tell the outside world your API is alive and what it can do.

Production APIs expose a /health endpoint so monitoring tools can verify the service is running. They also expose metadata endpoints so consumers know what capabilities are available. In this step, you'll add both and run a full integration test.

In this step, get ready to:
  • Add a /health endpoint that returns the API status and timestamp.
  • Add a /models endpoint that lists available AI models.
  • Run a full integration test across all your endpoints.
Add a /health endpoint

Monitoring tools, load balancers, and deployment platforms all ping a health endpoint to confirm your service is responding. The standard pattern returns a status, a timestamp, and a version number.

  • In VS Code, switch to models.py.
  • Find the ErrorResponse class at the bottom of the file.
  • Add the following HealthResponse model directly above ErrorResponse:
class HealthResponse(BaseModel):
    status: str
    timestamp: str
    version: str

What does this model do?

HealthResponse defines the exact JSON shape your health endpoint returns. Pydantic validates that every response includes all three fields before sending it to the client.

  • Save models.py.
  • Now switch to main.py to wire up the route.
  • Find this import block at the top of the file:
from fastapi import FastAPI, HTTPException

from ai_service import analyze_code
from models import (
    ErrorResponse,
    Issue,
    ReviewRequest,
    ReviewResponse,
)
  • Replace the entire import block with this updated version:
from datetime import datetime, timezone

from fastapi import FastAPI, HTTPException

from ai_service import analyze_code
from models import (
    ErrorResponse,
    HealthResponse,
    Issue,
    ReviewRequest,
    ReviewResponse,
)

What changed in the imports?

You added from datetime import datetime, timezone at the top so the health endpoint can stamp each response with the current UTC time.

You also added HealthResponse to the models import so FastAPI can use it as the response schema.

  • Find the root function that handles GET /.
  • Add the /health route directly below it by pasting this code after the root function:
@app.get("/health", response_model=HealthResponse)
def health_check():
    return HealthResponse(
        status="healthy",
        timestamp=datetime.now(timezone.utc).isoformat(),
        version="1.0.0",
    )

What does this endpoint do?

  • status reports whether the API process is alive and responding.
  • timestamp uses datetime.now(timezone.utc).isoformat() to return the current UTC time in ISO 8601 format.
  • version pins to 1.0.0 so consumers always know which release they are talking to.
  • Save main.py.
  • Your dev server auto-reloads. Open your browser and visit http://127.0.0.1:8000/health.

You should see a JSON response with "status": "healthy", a current UTC timestamp, and "version": "1.0.0".

Seeing a server error?

  • Check that from datetime import datetime, timezone is the very first line in main.py.
  • Confirm HealthResponse is listed inside the from models import (...) block and that you saved both files.
  • Look at the terminal running uvicorn for a traceback pointing to the missing import.

help me fix my /health endpoint import error

Add a /models endpoint

API consumers need to know which AI models your service supports. A /models endpoint documents your API's capabilities so clients can discover what's available without reading source code.

  • Switch back to models.py.
  • Add the following ModelInfo class directly below HealthResponse and above ErrorResponse:
class ModelInfo(BaseModel):
    model_id: str
    name: str
    description: str
    supports_code_review: bool

What does this model do?

ModelInfo describes a single AI model's metadata. The supports_code_review boolean lets consumers filter for models that handle code analysis.

  • Save models.py.
  • Switch back to main.py.
  • Find the from models import (...) block and add ModelInfo, on a new line between Issue, and ReviewRequest, so the import looks like this:
from models import (
    ErrorResponse,
    HealthResponse,
    Issue,
    ModelInfo,
    ReviewRequest,
    ReviewResponse,
)
  • Now add the /models route below the health_check function by pasting this code:
@app.get("/models", response_model=list[ModelInfo])
def list_models():
    return [
        ModelInfo(
            model_id="meta-llama/llama-3.3-70b-instruct:free",
            name="Llama 3.3 70B Instruct",
            description="Meta's powerful open-source model, free via OpenRouter",
            supports_code_review=True,
        ),
    ]

What does this endpoint do?

It returns a list of model objects describing each AI model the API can use. Right now there's one entry for the Llama 3.3 70B model. You could add more models here later without changing any other code.

  • Save main.py.
  • Visit http://127.0.0.1:8000/models in your browser.

You should see a JSON array with one object containing model_id, name, description, and supports_code_review fields.

Getting a validation error?

  • Confirm ModelInfo appears in the from models import (...) block in main.py.
  • Verify the class name in models.py is exactly ModelInfo (capital M, capital I).
  • Check that you saved both models.py and main.py after your edits.

help me debug my /models endpoint

Run a full integration test

You now have four endpoints: /, /health, /models, and /review. Time to run through the complete flow and confirm everything works end-to-end.

  • Open http://127.0.0.1:8000/docs in your browser to see the interactive Swagger UI.
  • Click the GET /health section, then click Try it out, then Execute.
  • Confirm the response shows "status": "healthy" with a current timestamp.
  • Click the GET /models section, click Try it out, then Execute.
  • Confirm you see the Llama 3.3 model entry in the response.

Now test the AI review flow.

  • Click the POST /review section, then click Try it out.
  • Replace the request body with this snippet containing an obvious bug:
{
  "code": "def is_even(n):\n    if n % 2 = 0:\n        return True\n    return False",
  "language": "python"
}
  • Click Execute.

Before you check the response, what do you expect the AI to flag in this code?

The AI should identify the = vs == bug in the condition. You'll see a response with an issues array, a summary, and model_used matching the ReviewResponse schema.

What if the AI returns a different number of issues?

AI models are non-deterministic. The AI might flag the missing == operator alone, or it might also suggest using return n % 2 == 0 as a cleaner one-liner. Both are valid responses. The important thing is that the response matches the structured JSON shape you defined.

Getting a 503 from the /review endpoint?

  • Check that your .env file contains a valid OPENROUTER_API_KEY value.
  • If you see a rate-limit message, wait 60 seconds and try again. Free accounts allow 20 requests per minute.
  • If you see an authentication error, double-check your API key at openrouter.ai/keys.

help me fix my 503 error from the /review endpoint

✔️ Awesome, I've got everything!

Great. Double check you've saved both models.py and main.py.

ⓧ I'd like to double check the full code

Your main.py should now contain these sections in order:

  • Imports: datetime, FastAPI, HTTPException, analyze_code, and all six model names (ErrorResponse, HealthResponse, Issue, ModelInfo, ReviewRequest, ReviewResponse).
  • The app = FastAPI(...) block with title, description, and version.
  • GET / route (root function).
  • GET /health route (health_check function).
  • GET /models route (list_models function).
  • POST /review route (review_code function with error handling).

Your models.py should contain these classes in order:

  • SupportedLanguage enum.
  • ReviewRequest model.
  • Issue model.
  • ReviewResponse model.
  • HealthResponse model (new).
  • ModelInfo model (new).
  • ErrorResponse model.

Scroll up and confirm each block is present in the correct order.

Your API is now production-ready with monitoring endpoints, structured AI reviews, and proper error handling. You've built the complete feature set of a professional code review service.

Secret mission

Add a /compare Endpoint

Your /review endpoint handles one snippet at a time. But what if you could compare the same algorithm written in two different languages side-by-side? Build a /compare endpoint that accepts two code snippets and returns a cross-language analysis.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses free OpenRouter models, so there are no ongoing costs.

Resources you used:

  • code-review-api project folder on your Desktop (includes virtual environment, source files, and .env).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep the project around as a portfolio piece you can push to GitHub.

  • Your project files stay on your Desktop in the code-review-api folder.
  • Restart the server anytime by activating the virtual environment and running uvicorn main:app --reload.

Pause - I'll come back to this later

Shut down running processes to free up your terminal. Your files stay on disk so you can resume anytime.

  • Stop the uvicorn server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate the virtual environment by running:
deactivate
  • To resume later, open a terminal in the code-review-api folder, activate the virtual environment, and run uvicorn main:app --reload.

Delete - I don't want to use this again

Remove all project files and start fresh. This cannot be undone.

  • Stop the uvicorn server by pressing Ctrl+C if it is still running.
  • Deactivate the virtual environment by running deactivate.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/code-review-api
  • On Windows, run this instead:
rmdir /s /q "$env:USERPROFILE\Desktop\code-review-api"
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see code-review-api in the output.

Nice Work!

Nice Work!

You just built a production-pattern AI backend from scratch. You have a working REST API that accepts any code snippet and returns a structured, AI-powered review complete with severity ratings, line references, and actionable suggestions.

Here's what you learned:

  • Built a complete REST API with FastAPI that accepts code snippets and returns structured AI-generated reviews with validated request and response models.
  • Integrated with OpenRouter to access free LLMs through a production-grade SDK, including a fallback JSON parser that handles messy AI output.
  • Added error handling for real-world edge cases like malformed responses, rate limits, and invalid inputs, plus monitoring endpoints following production API patterns.
  • Secret Mission: Designed a multi-input /compare endpoint with a sophisticated comparison prompt that reviews the same logic across two different languages side-by-side.

Ready to quiz yourself?