Build an AI Code Reviewer with FastAPI
Wire a FastAPI backend to Claude to build a structured code review tool.
Introduction
30 Second Summary
Code reviews catch bugs, security issues, and style problems before they reach production. But getting timely feedback from a human reviewer can take hours or even days.
In this project, you will build a web application that delivers instant, AI-powered code reviews. You paste code into a form, click a button, and get a color-coded report with categorized feedback in seconds.
What You'll Build
You run your app, paste in a Python function with a sneaky bug, and seconds later a polished report appears with color-coded cards sorting every issue by category and severity.
By the end of this project, you'll have:
- A live code review tool where you paste any code and instantly see color-coded cards grouping issues into bugs, security, performance, and style with severity badges.
- Hands-on experience with structured prompt engineering that forces Claude to return parseable JSON instead of a wall of text, so your app can render it as a professional report.
- A full request cycle you can trace end-to-end: browser form to FastAPI backend to Claude API to a styled Jinja2 results page.
- Secret Mission: Language and depth controls that visibly change how the AI analyzes your code.
Are there any prerequisites?
You should have basic Python knowledge and a general understanding of how APIs work. No experience with FastAPI or AI APIs is required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project
You have committed to building an AI code reviewer. Before you can wire anything to Claude, you need a working Python environment with the right packages installed and a verified connection to the API.
In this step, you will create a project folder, initialize a Git repository, install the Python packages your app depends on, grab an API key, and prove that Claude can hear you.
In this step, get ready to:
- Create a project folder with a Git repository, Python virtual environment, and installed dependencies.
- Get an Anthropic API key and store it safely.
- Run a test script that confirms Claude responds.
Create the project folder and install dependencies
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (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 ai-code-reviewer
cd ai-code-reviewer
git init
You should see Initialized empty Git repository in the output.
Seeing 'git: command not found'?
If git is not recognized, you need to install Git first. Download it from git-scm.com/downloads, run the installer, then reopen your terminal and try again.
help me install Git on my computer
- Create a .gitignore file to keep sensitive and generated files out of version control by running:
echo ".env" > .gitignore
echo ".venv/" >> .gitignore
echo "__pycache__/" >> .gitignore
What is .gitignore?
Any file or folder listed in .gitignore is invisible to Git. Your API key (.env), virtual environment (.venv/), and Python cache (__pycache__/) will never be committed to your repository.
- Create a Python virtual environment by running:
python3 -m venv .venv
Windows users
If python3 is not recognized on Windows, use python instead. Both point to your Python 3.12 installation.
- Confirm the virtual environment was created by running:
ls
You should see a .venv folder listed in the output.
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python, so different projects never conflict with each other.
- Activate the virtual environment by running:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
Your terminal prompt now starts with (.venv). This confirms the virtual environment is active.
- Install the project dependencies by running:
pip install "fastapi[standard]" anthropic python-dotenv
You should see output ending with a line like Successfully installed followed by a list of package names.
What are these packages?
- fastapi[standard] is the web framework for your backend. The [standard] extras include the uvicorn server and form-handling support.
- anthropic is the official Python SDK for calling the Claude API.
- python-dotenv loads environment variables from a .env file so you can keep secrets out of your code.
🙋♀️ Seeing a permissions error?
Make sure your (.venv) prefix is showing in your terminal. If it is not, re-run the activate command above.
help me fix a pip install permissions error in my virtual environment
Get your Anthropic API key
The Anthropic Python SDK needs an API key to authenticate your requests to Claude. You will create one in the Claude Console and store it in a local file that never gets committed to version control.
- Open your browser and go to platform.claude.com/settings/keys.
- Create a free account or sign in if you already have one.
- Click Create Key.
- Give your key a name (e.g. code-reviewer) and click Create Key to confirm.
- Copy the key immediately. You will not be able to see it again after you close the dialog.
What about cost?
New Anthropic accounts receive free credits to test the API. This project uses Claude Sonnet 5, which costs $2 per million input tokens and $10 per million output tokens. A full run of this project costs a few cents at most.
- Open your code editor.
- Create a new file called .env in the ai-code-reviewer folder on your Desktop.
- Paste the following into .env, replacing your-api-key-here with the key you just copied:
ANTHROPIC_API_KEY=your-api-key-here
- Save .env.
Your key is already protected
Your .gitignore file already lists .env, so Git will never track your API key. If you add other sensitive files later, add them to .gitignore as well.
Create and run the test script
Time to prove your connection works. You will write a short script that sends a message to Claude and prints the response.
- Create a new file called test_claude.py in your ai-code-reviewer folder and paste the following code:
from dotenv import load_dotenv
import anthropic
# Load the API key from .env into the environment
load_dotenv()
# Create the Anthropic client (reads ANTHROPIC_API_KEY automatically)
client = anthropic.Anthropic()
# Send a test message to Claude Sonnet 5
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[
{"role": "user", "content": "Say hello and confirm you can review code."}
]
)
# Print each text block from Claude's response
for block in message.content:
if block.type == "text":
print(block.text)
What does this code do?
- load_dotenv() reads your .env file and loads ANTHROPIC_API_KEY into the environment. The SDK picks it up automatically.
- client.messages.create() sends a message to Claude Sonnet 5 and returns a response object.
- The for loop extracts text blocks from the response and prints them to your terminal.
- Save test_claude.py.
Before you run this, what do you think Claude will reply with? Take a guess.
- Run the test script in your terminal:
python test_claude.py
You should see Claude respond with a friendly greeting confirming it can review code.
Seeing an authentication error?
- Double-check that your .env file is in the same folder as test_claude.py. Both should be at the root of ai-code-reviewer.
- Make sure you pasted the full API key with no extra spaces or line breaks.
- Confirm the variable name is exactly ANTHROPIC_API_KEY (all caps, underscores).
help me fix an Anthropic API authentication error in my Python script
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the full .env file:
ANTHROPIC_API_KEY=your-api-key-here
Here is the full test_claude.py file:
from dotenv import load_dotenv
import anthropic
load_dotenv()
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[
{"role": "user", "content": "Say hello and confirm you can review code."}
]
)
for block in message.content:
if block.type == "text":
print(block.text)
Your environment is ready and Claude is listening. Next up, you will build the web page where users paste code for review.
Build the Code Input Page
Your API key is working and Claude is responding. Now you need a way for users to actually submit code for review.
In this step, you will build the web interface that collects code input. You will create a FastAPI application that serves an HTML page with a form, and style it with a dark theme.
In this step, get ready to:
- Create a FastAPI application with static file serving and template rendering.
- Build an HTML form where users can paste code for review.
- Style the page with a dark theme and confirm it loads in your browser.
Create the FastAPI application
Your app needs two directories to function: one for HTML templates and one for static assets like CSS.
- Create the templates and static directories by running these commands:
mkdir templates static
- Confirm the directories were created by running:
ls
You should see static, templates, and your existing files (.env, test_claude.py) in the output.
- Create a new file called main.py in your project folder and add the following code:
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import anthropic
load_dotenv()
app = FastAPI()
# Mount the static directory so CSS files are served at /static/
app.mount("/static", StaticFiles(directory="static"), name="static")
# Tell Jinja2 where to find HTML templates
templates = Jinja2Templates(directory="templates")
# Create the Anthropic client (reads ANTHROPIC_API_KEY from env)
client = anthropic.Anthropic()
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(request=request, name="index.html")
What does this code do?
- app.mount("/static", ...) makes any file in the static/ folder available at URLs starting with /static/.
- Jinja2Templates(directory="templates") tells FastAPI to look in the templates/ folder when rendering HTML pages.
- The @app.get("/") route serves the home page whenever someone visits the root URL.
- client = anthropic.Anthropic() creates the Claude client you will use in the next step to send code for review.
- Save main.py.
Seeing a red underline on the imports?
Your editor might show import warnings if it is not using the virtual environment's Python interpreter. Make sure your editor's Python path points to the .venv folder you created in Step 1.
help me fix my editor's Python interpreter
Create the HTML form
The home page needs a form where users can paste code. The form will submit to a /review endpoint that you will build in the next step.
- Create a new file called index.html inside the templates folder and add this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Code Reviewer</title>
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>AI Code Reviewer</h1>
<p class="subtitle">Paste your code below and get instant feedback powered by Claude.</p>
<form action="/review" method="post">
<textarea name="code" placeholder="Paste your code here..." rows="15" required></textarea>
<button type="submit">Review My Code</button>
</form>
</div>
</body>
</html>
What does this code do?
- {{ url_for('static', path='/style.css') }} is Jinja2 syntax that generates the correct URL to your CSS file at runtime.
- The <textarea name="code"> creates a large text input area. The name="code" attribute is how FastAPI reads the submitted value on the backend.
- The form's action="/review" and method="post" send the code to a POST endpoint you will create in Step 3.
- Save templates/index.html.
Before you add any styling, start the server to confirm the page loads.
- Start the FastAPI development server by running:
uvicorn main:app --reload
What does this command do?
uvicorn is the web server that runs your FastAPI app. main:app tells it to look for the app object in main.py. The --reload flag automatically restarts the server whenever you save a file.
- Open http://127.0.0.1:8000 in your browser.
You should see a plain white page with the heading "AI Code Reviewer", a subtitle, a large text area, and a button. It works but looks bare without styling.
Seeing an error in the terminal?
- If you see Directory 'static' does not exist, make sure you ran mkdir templates static earlier. Both directories must exist before the server starts.
- If you see TemplateNotFound, confirm that index.html is inside the templates/ folder, not in your project root.
help me debug my FastAPI server startup error
Add dark-theme styling
The page works but looks unstyled. You will add CSS to give it a dark theme with proper spacing and a polished look.
- Create a new file called style.css inside the static folder and add the page layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #ffffff;
}
.subtitle {
color: #a0a0b0;
margin-bottom: 2rem;
}
What does this code do?
The reset block (* { margin: 0; ... }) removes browser default spacing so all elements start from a consistent baseline. The body sets the dark background (#1a1a2e) and light text color.
.container centers the content at a max width of 800px so it does not stretch across the full screen on wide monitors.
- Below the .subtitle block you just added, paste the form element styles:
textarea {
width: 100%;
padding: 1rem;
border: 2px solid #2a2a4a;
border-radius: 8px;
background-color: #16213e;
color: #e0e0e0;
font-family: "Fira Code", "Courier New", monospace;
font-size: 0.9rem;
resize: vertical;
margin-bottom: 1rem;
}
textarea:focus {
outline: none;
border-color: #4a9eff;
}
button, .back-button {
display: inline-block;
padding: 0.75rem 2rem;
background-color: #4a9eff;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s;
}
button:hover, .back-button:hover {
background-color: #3a8eef;
}
What does this code do?
The textarea styles give the code input area a dark background with a monospace font, matching the look of a code editor. The border highlights blue on focus so users know the field is active.
The button styles create a bright blue call-to-action that stands out against the dark background.
- Save static/style.css.
- Switch back to your browser and refresh the page at http://127.0.0.1:8000.
What do you expect the page to look like now that you have added CSS?
You should see a dark-themed page with a large textarea and a bright blue "Review My Code" button. The text area uses a monospace font and the whole layout is centered on the page.
Page still looks unstyled?
- Check that your file is saved as static/style.css (inside the static folder, not the project root).
- Try a hard refresh: Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). Browsers cache CSS files aggressively.
- Open DevTools (F12) and check the Console tab for a 404 on style.css. If you see one, the file path or name does not match.
help me figure out why my CSS is not loading
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm you have both CSS chunks in static/style.css in order: the page layout styles (reset, body, container, h1, subtitle) followed by the form element styles (textarea, textarea:focus, button). Here are the other two files in full:
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import anthropic
load_dotenv()
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
client = anthropic.Anthropic()
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(request=request, name="index.html")
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Code Reviewer</title>
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>AI Code Reviewer</h1>
<p class="subtitle">Paste your code below and get instant feedback powered by Claude.</p>
<form action="/review" method="post">
<textarea name="code" placeholder="Paste your code here..." rows="15" required></textarea>
<button type="submit">Review My Code</button>
</form>
</div>
</body>
</html>
Your web interface is live. Next up, you will wire the form to Claude so submitting code actually returns a review.
Connect to Claude with a Basic Prompt
Your code input page looks great, but right now clicking Review My Code goes nowhere. The form submits to /review, but that endpoint does not exist yet.
In this step, you will wire the form to Claude so submitted code actually gets reviewed. You will also see firsthand what happens when you use a naive prompt with no structure.
In this step, get ready to:
- Add a POST endpoint that sends submitted code to Claude.
- Create a results page that displays Claude's response.
- Observe the limitation of an unstructured prompt.
Add the POST /review endpoint
When the learner clicks Review My Code, the form sends a POST request to /review with the textarea contents. You need an endpoint to catch that request, build a prompt, and forward it to Claude.
- In main.py, find the import line that reads:
from fastapi import FastAPI, Request
- Add Form to that import so it looks like this:
from fastapi import FastAPI, Request, Form
- Scroll to the bottom of main.py, below the existing home function, and add the following endpoint:
@app.post("/review", response_class=HTMLResponse)
async def review_code(request: Request, code: str = Form(...)):
# Build a simple prompt with the submitted code
prompt = f"Review this code and list any issues:\n\n{code}"
# Send the prompt to Claude
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
# Extract text from Claude's response blocks
response_text = ""
for block in message.content:
if block.type == "text":
response_text += block.text
return templates.TemplateResponse(
request=request,
name="results.html",
context={"response_text": response_text, "code": code}
)
What does this code do?
Form(...) tells FastAPI to read the code field from the submitted form data. The three dots mean the field is required.
The prompt is intentionally simple. It just asks Claude to "Review this code and list any issues" with no formatting guidance. You will see why this matters shortly.
The response loop iterates through Claude's content blocks and extracts the text, then passes it to a template for rendering.
- Save main.py.
- Check your terminal where the server is running. You should see it reload and print Application startup complete. with no errors.
Seeing an error in the terminal?
If you see IndentationError or SyntaxError, check that the new function is at the same indentation level as the home function above it. Make sure there is a blank line separating the two functions.
If you see ImportError, confirm your import line now includes Form after Request.
help me fix a server reload error in my FastAPI app
✔️ Awesome, I've got everything!
Great. Double check you saved main.py and the server reloaded without errors.
ⓧ I'd like to double check the full code
Your main.py should now have these sections in order:
- Imports at the top (including Form in the fastapi import).
- load_dotenv(), app setup, static files mount, templates, and client initialization.
- The home GET route (unchanged from Step 2).
- The new review_code POST route you just added.
Scroll up and confirm each chunk is present in that order.
Create the results template
The endpoint renders results.html, but that file does not exist yet. You need a page that shows Claude's response and the original code.
- Create a new file called results.html inside your templates folder.
- Paste the following content into templates/results.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Review Results - AI Code Reviewer</title>
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>Review Results</h1>
<div class="summary">
<pre>{{ response_text }}</pre>
</div>
<div class="code-reviewed">
<h3>Code Reviewed</h3>
<pre><code>{{ code }}</code></pre>
</div>
<a href="/" class="back-button">Review More Code</a>
</div>
</body>
</html>
What does this template do?
The {{ response_text }} variable displays Claude's raw response inside a <pre> tag. The {{ code }} variable shows the original code the user submitted.
The Review More Code link sends the user back to the home page so they can submit another snippet.
- Save templates/results.html.
- Now add styles for the results page. In static/style.css, scroll to the bottom and add the following:
.summary {
background-color: #16213e;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border-left: 4px solid #4a9eff;
}
.code-reviewed {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.code-reviewed h3 {
margin-bottom: 0.75rem;
color: #a0a0b0;
}
.code-reviewed pre {
background-color: #16213e;
padding: 1.25rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
}
.back-button {
margin-top: 1rem;
}
What do these styles do?
The .summary class gives Claude's response a dark card with a blue left border so it stands out visually. The .code-reviewed section displays the original code in a similar dark container.
- Save static/style.css.
- Check your terminal. The server should reload again with no errors.
Server not reloading?
Make sure results.html is saved inside the templates folder, not the project root. If it is in the wrong location, the endpoint will throw a TemplateNotFound error when you try to submit code.
help me fix a TemplateNotFound error in FastAPI
Test with buggy code
Everything is wired up. Time to see if Claude can actually review your code.
- Switch to your browser and go to http://127.0.0.1:8000.
- Paste the following buggy Python function into the textarea:
def calculate_average(numbers):
total = 0
for n in numbers:
total += n
average = total / len(numbers)
return average
Before you click Review My Code, think about this: what do you expect the response to look like? Will it be neatly organized, or something else?
- Click Review My Code.
After a few seconds, you land on the results page. Claude identifies the bug (dividing by zero when the list is empty) and likely mentions other improvements. But look at the format of the response.
What should I see?
Claude's feedback arrives as a wall of unformatted text. There are no categories, no severity levels, and no visual hierarchy. If this function had five issues across different categories, you would have to read every sentence to find the critical bug.
This is the limitation of a naive prompt. The AI gives useful information, but the format is not designed for quick scanning. The next step fixes this with structured prompting.
You have a working code review flow from browser to Claude and back. The AI catches real issues, but the response is a single block of prose. In the next step, you will use structured prompting to make Claude return categorized JSON so you can render a professional report.
Structure the AI Response
Your code review tool works, but the raw text response from Claude is hard to scan. Everything comes back as a single wall of prose with no visual hierarchy.
Professional code review tools categorize issues at a glance: bugs in red, security warnings in orange, performance suggestions in green. In this step, you will upgrade your prompt to make Claude return structured JSON, parse that response, and render a polished report with color-coded cards.
In this step, get ready to:
- Replace the basic prompt with a structured JSON prompt that tells Claude exactly what shape to return.
- Parse the JSON response with a graceful fallback for malformed data.
- Rebuild the results template and stylesheet to display color-coded issue cards.
Upgrade the prompt to return structured JSON
The key insight behind structured prompting is that you can tell an LLM exactly what JSON shape to return. Instead of asking for free-form text, you provide a schema and instruct the model to return nothing else.
- In main.py, find the imports at the top of the file.
- Find this line:
from dotenv import load_dotenv
- Add import json on a new line directly above it, so the top of your file looks like this:
import json
from dotenv import load_dotenv
- Now scroll down to the review_code function and find the prompt line:
prompt = f"Review this code and list any issues:\n\n{code}"
- Replace that single line with the following structured prompt:
prompt = f"""Review the following code and provide feedback in JSON format.
Return ONLY valid JSON with this exact structure, no other text:
{{
"summary": "A one-sentence overall assessment",
"issues": [
{{
"category": "bug" | "security" | "performance" | "style",
"severity": "high" | "medium" | "low",
"line_hint": "the relevant code snippet",
"description": "what the issue is and how to fix it"
}}
]
}}
If the code has no issues, return an empty issues array.
Code to review:
```
{code}
```"""
What does this prompt do?
The prompt defines an exact JSON schema with four issue categories (bug, security, performance, style) and three severity levels (high, medium, low).
The instruction "Return ONLY valid JSON" prevents Claude from wrapping the response in markdown code fences or adding explanatory text around it. The double curly braces {{ are Python f-string escapes that produce literal { characters in the output.
- Still in the review_code function, find the return statement at the bottom:
return templates.TemplateResponse(
request=request,
name="results.html",
context={"response_text": response_text, "code": code}
)
- Replace that entire return block with JSON parsing and an updated return that passes structured data to the template:
try:
review = json.loads(response_text)
except json.JSONDecodeError:
review = {
"summary": "Could not parse structured response.",
"issues": [],
"raw_response": response_text
}
return templates.TemplateResponse(
request=request,
name="results.html",
context={"review": review, "code": code}
)
What does this code do?
The json.loads() call converts Claude's text response into a Python dictionary. If Claude returns malformed JSON for any reason, the except block creates a fallback dictionary with the raw text so you can still display something useful.
The template context now passes a review dictionary instead of raw text. This means results.html must be updated to match before you can test.
- Save main.py.
- Check the terminal where your server is running. The --reload flag restarts the server on every save. You should see a line indicating a successful reload with no syntax errors.
Seeing a syntax error in the terminal?
Check that the f-string triple quotes f"""...""" are correct. A missing opening or closing set of triple quotes will cause a SyntaxError.
Also confirm the double curly braces {{ and }} are present around the JSON keys in the prompt. Single braces inside an f-string cause a format error.
help me fix a syntax error in my structured prompt f-string
Rebuild the results template for structured data
The template now receives a review dictionary instead of plain text. You need to rebuild results.html to render the summary, category stats, and individual issue cards.
- Open templates/results.html and delete all existing content.
- Paste the following as the top section of the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Review Results - AI Code Reviewer</title>
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>Review Results</h1>
<div class="summary">
<p>{{ review.summary }}</p>
</div>
{% if review.issues %}
<div class="stats">
<span class="stat bug">Bugs: {{ review.issues | selectattr("category", "equalto", "bug") | list | length }}</span>
<span class="stat security">Security: {{ review.issues | selectattr("category", "equalto", "security") | list | length }}</span>
<span class="stat performance">Performance: {{ review.issues | selectattr("category", "equalto", "performance") | list | length }}</span>
<span class="stat style-cat">Style: {{ review.issues | selectattr("category", "equalto", "style") | list | length }}</span>
</div>
What does the stats section do?
The selectattr filter is a Jinja2 built-in that filters a list by attribute value. It counts how many issues fall into each category and displays the totals as a quick-glance summary bar.
- Directly below the stats section you just pasted, add the issue cards loop and the rest of the page:
{% for issue in review.issues %}
<div class="issue {{ issue.category }}">
<div class="issue-header">
<span class="badge {{ issue.category }}">{{ issue.category }}</span>
<span class="severity {{ issue.severity }}">{{ issue.severity }}</span>
</div>
<p class="description">{{ issue.description }}</p>
{% if issue.line_hint %}
<code class="line-hint">{{ issue.line_hint }}</code>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="no-issues">
<p>No issues found. Your code looks good!</p>
</div>
{% endif %}
{% if review.raw_response %}
<div class="raw-response">
<h3>Raw Response</h3>
<pre>{{ review.raw_response }}</pre>
</div>
{% endif %}
<div class="code-reviewed">
<h3>Code Reviewed</h3>
<pre><code>{{ code }}</code></pre>
</div>
<a href="/" class="back-button">Review More Code</a>
</div>
</body>
</html>
What does the issue loop do?
Each issue gets its own card with a colored border based on issue.category. The badge shows the category name, the severity label shows how critical the issue is, and line_hint highlights the specific code snippet causing the problem.
The {% else %} block handles the case where Claude finds no issues. The raw_response section only renders if JSON parsing failed, showing the fallback text.
- Save templates/results.html.
- Switch to your browser and navigate to http://127.0.0.1:8000.
- Paste the same buggy code you tested with in the previous step and click Review My Code.
- You should see a summary sentence at the top, a stats bar counting issues by category, and individual issue cards. The cards won't have full color-coding yet, but the structure is there.
Seeing a server error instead of results?
If the page shows an Internal Server Error, check the terminal for a traceback. A common cause is a mismatch between the variable name in main.py (review) and the template (review.summary).
Also confirm you deleted the old results.html content entirely before pasting. Leftover {{ response_text }} references will cause a Jinja2 UndefinedError.
help me fix a Jinja2 template error in my results page
Add category color-coding to the stylesheet
The issue cards render correctly but lack visual distinction between categories. Color-coding makes the report scannable at a glance: red for bugs, orange for security, green for performance, blue for style.
- Open static/style.css and add the following styles at the bottom of the file, after the existing .back-button rule:
.stats {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.stat {
padding: 0.4rem 0.8rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 600;
}
.stat.bug { background-color: #3d1f1f; color: #ff6b6b; }
.stat.security { background-color: #3d2f1f; color: #ffa94d; }
.stat.performance { background-color: #1f3d2f; color: #69db7c; }
.stat.style-cat { background-color: #1f2f3d; color: #74c0fc; }
.issue {
background-color: #16213e;
padding: 1.25rem;
border-radius: 8px;
margin-bottom: 1rem;
border-left: 4px solid #555;
}
.issue.bug { border-left-color: #ff6b6b; }
.issue.security { border-left-color: #ffa94d; }
.issue.performance { border-left-color: #69db7c; }
.issue.style { border-left-color: #74c0fc; }
.issue-header {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 0.75rem;
}
What do these styles do?
Each category gets a distinct color: red (#ff6b6b) for bugs, orange (#ffa94d) for security, green (#69db7c) for performance, and blue (#74c0fc) for style.
The .issue cards use a colored left border to signal the category at a glance. The .stats bar uses matching background tints so the count pills look cohesive with the cards below.
- Continue adding the badge and severity styles directly below:
.badge {
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.badge.bug { background-color: #3d1f1f; color: #ff6b6b; }
.badge.security { background-color: #3d2f1f; color: #ffa94d; }
.badge.performance { background-color: #1f3d2f; color: #69db7c; }
.badge.style { background-color: #1f2f3d; color: #74c0fc; }
.severity {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.severity.high { color: #ff6b6b; }
.severity.medium { color: #ffa94d; }
.severity.low { color: #a0a0b0; }
.description {
line-height: 1.5;
}
.line-hint {
display: block;
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
background-color: #0f1729;
border-radius: 4px;
font-size: 0.85rem;
color: #a0a0b0;
}
- Finally, add the edge-case styles for when no issues are found and for the raw response fallback:
.no-issues {
background-color: #1f3d2f;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #69db7c;
color: #69db7c;
}
.raw-response {
margin-top: 1.5rem;
background-color: #16213e;
padding: 1.25rem;
border-radius: 8px;
}
.raw-response pre {
white-space: pre-wrap;
font-size: 0.85rem;
}
What are these for?
The .no-issues style shows a green success banner when Claude finds nothing wrong with the submitted code. The .raw-response style is the fallback display when JSON parsing fails, keeping the raw text readable.
- Save static/style.css.
Before you test, predict what you expect to see when you submit the same buggy code from the previous step. Will the results look different from the wall of text you saw before?
- Switch to your browser and refresh http://127.0.0.1:8000.
- Paste a short piece of buggy Python code (for example, a function with an obvious bug, a missing import, or an SQL injection vulnerability) and click Review My Code.
Instead of a wall of text, you now see color-coded cards grouped by category with severity badges and actionable descriptions. Each card has a colored left border matching its category.
Cards showing but no colors?
Make sure you saved style.css after adding all three groups of new styles. Do a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to bypass the CSS cache.
If cards appear but the left borders are gray instead of colored, check that the CSS class names match the template. The template uses {{ issue.category }} which outputs values like bug or security. Your CSS must use .issue.bug, .issue.security, etc.
help me debug why my color-coded CSS styles aren't applying to the issue cards
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: main.py, templates/results.html, and static/style.css.
ⓧ I'd like to double check the full code
Scroll up and confirm these pieces are in place, in order:
- main.py: import json at the top, the structured JSON prompt inside review_code, and the try/except json.JSONDecodeError block replacing the old return.
- templates/results.html: The summary div, stats bar with selectattr filters, the {% for issue in review.issues %} loop rendering cards, and the closing section with raw_response and code-reviewed blocks.
- static/style.css: All original styles from before, plus the three new groups: stats/issue layout, badge/severity/line-hint colors, and no-issues/raw-response fallbacks.
Your code reviewer now delivers professional, scannable feedback. Next up, you will extend it with a Secret Mission that lets users choose the review language and depth.
Secret mission
Add Language Selection and Review Depth
Your code reviewer treats every submission the same way. But a Python review should flag different patterns than a JavaScript review, and sometimes you want a quick scan instead of a deep audit. Can you add controls that change how Claude thinks?
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, so there are no ongoing costs unless you actively make API calls to Claude.
Resources you used:
- Your project folder on the Desktop (includes main.py, test_claude.py, templates, static assets, and virtual environment).
- Your Anthropic API key (stored in .env and active in the Anthropic Console).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment further with prompts, languages, and depth settings.
- Your FastAPI server only runs when you start it manually. No background processes accumulate charges.
- Claude API charges are pay-per-use. You are only billed when you submit a review.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your files stay on disk ready to pick up later.
- Switch to the terminal where your FastAPI server is running.
- Press Ctrl+C to stop the server.
- Your project folder and virtual environment remain on disk. When you come back, reactivate the virtual environment and run uvicorn main:app --reload to start again.
Delete - I don't want to use this again
Remove all project files and revoke your API key so nothing lingers on your machine or in the Anthropic Console.
- Switch to the terminal where your FastAPI server is running and press Ctrl+C to stop it.
- Delete your project folder from the Desktop by running:
macOS
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
Windows
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
- Revoke your API key in the Anthropic Console. Open console.anthropic.com, navigate to your API Keys page, and delete the key you created for this project.
Nice Work!
Nice Work!
You just built a full-stack AI-powered code review tool from scratch. Your FastAPI backend takes code from a browser form, sends it to Claude, and renders structured, color-coded feedback in seconds.
You've learned how to:
- Build a full-stack web application with FastAPI that serves Jinja2 templates, handles form submissions, and mounts static assets.
- Integrate the Claude API using the official Anthropic Python SDK with proper environment variable management through python-dotenv.
- Design structured prompts that return parseable JSON instead of free-form text, then render the results as a professional UI with color-coded categories and severity badges.
- Secret Mission: Parameterized AI behavior by injecting language and depth variables into prompts, proving that prompt design directly controls output quality.
Ready to quiz yourself?