Skip to content

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:

shell
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:

shell
git remote add solutions https://github.com/nnja/python-for-pros-solutions.git
git fetch solutions

Set up the starting state for this exercise:

shell
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:

src/release_tracker/main.py
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.

src/release_tracker/main.py
@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:

tests/test_main.py
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:

shell
uv run pytest

Success

If all four tests pass, your slug-based search endpoint and 404 handling are working correctly.

Compare with the Solution

shell
git diff solutions/ch04 -- src/ tests/