Exercise: Build a Basic API
In this exercise, you'll add a new endpoint and a test for it and add some error handling.
Before You Start
Make sure you have the course repo checked out:
git clone https://github.com/nnja/python-for-pros.git
cd python-for-pros
For each exercise, you'll grab a local copy of the branch with all the files you need.
Add the solutions repo as a second remote so you can diff against it when you get stuck:
git remote add solutions https://github.com/nnja/python-for-pros-solutions.git
git fetch solutions
Set up the starting state for this exercise:
git fetch
git checkout -b my-ch04 --no-track origin/ch04
uv sync
code . # open in vs code, trust the authors
The --no-track flag prevents an accidental git pull from overwriting your work.
Files that changed in this chapter:
src/release_tracker/main.py(new, you'll fill in the TODO bodies)tests/test_main.py(new, you'll update one test)pyproject.toml(modified,fastapi[standard]added)
1. Add a 404 to get_project
Right now, requesting a project that doesn't exist (like /projects/999) returns a 500 error because mock_database.get(project_id) returns None and that doesn't satisfy ProjectRead. Fill in the EXERCISE STEP 1 TODO in src/release_tracker/main.py so get_project raises an HTTPException with a 404 status code instead.
The stub in your branch already imports HTTPException and status from fastapi:
from fastapi import FastAPI, HTTPException, status
# ... (existing code) ...
@app.get("/projects/{project_id}", response_model=ProjectRead)
def get_project(project_id: int):
project = mock_database.get(project_id)
# EXERCISE STEP 1:
# If `not project`, raise an HTTPException with status code
# status.HTTP_404_NOT_FOUND and detail "Project not found".
return project
Status Codes
Notice the status import we're providing in the stub. Instead of hardcoding 404, we use FastAPI's status.HTTP_404_NOT_FOUND constant. This prevents typos, enables autocomplete, and makes it easier to search your codebase for specific HTTP responses later.
2. Add a Slug-Based Search Endpoint
In the chapter, we wrote list_projects(name: str | None = None) to filter projects by their name. In the exercise stub, this is already updated to filter by slug instead. Fill in the EXERCISE STEP 2 TODO so the function filters by slug.
@app.get("/projects", response_model=list[ProjectRead])
def list_projects(slug: str | None = None):
projects = list(mock_database.values())
# EXERCISE STEP 2:
# If slug is None, return all projects.
# Otherwise, return only the projects whose `slug` attribute equals the given slug.
return projects
Notice we've added response_model=list[ProjectRead] to the decorator, just like we did with get_project. This gives us Pydantic validation on every item we return: if any project in the list is missing a required field like slug, FastAPI raises a ResponseValidationError instead of sending a broken response.
3. Update Your Tests
Open tests/test_main.py. You'll see four tests, three of which are already set up correctly. The exception is test_list_projects_by_slug, which still sends a name query parameter from the chapter's old version. Update it to send a slug parameter so it matches your new endpoint:
from fastapi.testclient import TestClient
from release_tracker.main import app
client = TestClient(app)
def test_list_projects():
response = client.get("/projects")
assert response.status_code == 200
assert len(response.json()) == 3
def test_list_projects_by_slug():
# EXERCISE STEP 3:
# Update this test to send `slug=api-v2` instead of `name=API v2`,
# so it matches the new slug-based search endpoint.
response = client.get("/projects", params={"name": "API v2"})
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["slug"] == "api-v2"
def test_get_project():
response = client.get("/projects/1")
assert response.status_code == 200
assert response.json()["name"] == "Frontend Redesign"
def test_get_project_not_found():
response = client.get("/projects/999")
assert response.status_code == 404
assert response.json()["detail"] == "Project not found"
Verification
Run the tests:
uv run pytest
Success
If all four tests pass, your slug-based search endpoint and 404 handling are working correctly.
Compare with the Solution
git diff solutions/ch04 -- src/ tests/