CHAPTER 52 · ADVANCED4 min read

Build a web API with FastAPI

Turn Python functions into a documented HTTP contract with validated requests, explicit status codes, and tests.

Python 3.12+Free to readModule 12 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Map resources and operations to HTTP methods, paths, and status codes.
  • Validate request and response bodies with Pydantic models.
  • Test the API through its public interface with TestClient.

01HTTP is a contract

An API is a promise about methods, paths, request bodies, response bodies, and status codes. GET reads, POST creates, PUT or PATCH updates, and DELETE removes. A successful creation usually returns 201; a missing resource returns 404; invalid input returns 422. The HTTP method and status are part of the interface, not decoration added after the code works.

FastAPI turns a decorated function into an operation and generates OpenAPI documentation from the annotations. The function name and docstring should describe one resource operation. Keep transport concerns at the route boundary and call ordinary Python functions for domain work. That way the same rule can be tested without starting a server.

main.pyPython
from fastapi import FastAPIapp = FastAPI(title="Task API", version="1.0")@app.get("/health")def health():    return {"status": "ok"}

02Model the boundary with Pydantic

Pydantic models describe the shape of data entering and leaving the API. Type annotations become validation rules, and Field adds length or range constraints. A request model should contain only fields a client may send. A response model can add server-owned fields such as an id while preventing accidental internal data from leaking.

FastAPI validates a request before calling the route. A malformed body produces a structured 422 response instead of a TypeError deep in your code. Keep validation at the boundary and keep domain invariants in the domain layer. If a title must be unique, that is a rule about stored tasks, not a property of the JSON shape alone.

models.pyPython
from pydantic import BaseModel, Fieldclass TaskIn(BaseModel):    title: str = Field(min_length=1, max_length=80)    done: bool = Falseclass TaskOut(TaskIn):    id: inttask = TaskIn(title="Ship the API")print(task.model_dump())print(TaskOut(id=1, **task.model_dump()).model_dump())
Outputstdout
{'title': 'Ship the API', 'done': False}
{'title': 'Ship the API', 'done': False, 'id': 1}

03Routes, dependencies, and errors

A route should translate HTTP into a domain operation and translate the result back. Use status_code on the decorator for a successful creation, response_model to enforce the public shape, and HTTPException for a missing resource. Avoid returning different shapes from the same endpoint; clients need a stable contract.

A dependency is a reusable function that provides something a route needs, such as configuration, a database session, or the current user. FastAPI resolves it before the route runs. For a small in-memory example, a module-level dictionary is enough to demonstrate the flow, but the route boundary remains the same when the storage becomes SQLite or PostgreSQL.

tasks.pyPython
from fastapi import FastAPI, HTTPExceptionfrom models import TaskIn, TaskOutapp = FastAPI()tasks = {}next_id = 1@app.post("/tasks", status_code=201, response_model=TaskOut)def create_task(task: TaskIn):    global next_id    item = TaskOut(id=next_id, **task.model_dump())    tasks[item.id] = item    next_id += 1    return item@app.get("/tasks/{task_id}", response_model=TaskOut)def get_task(task_id: int):    if task_id not in tasks:        raise HTTPException(status_code=404, detail="task not found")    return tasks[task_id]

04Test and ship the API

TestClient calls the application through its public HTTP interface without opening a network port. A test can assert the status code, the JSON body, and the error shape. Cover the happy path, a missing id, a validation failure, and any state change. Those checks protect the contract that clients depend on.

Run the app with Uvicorn during development, and read the generated documentation at /docs. Keep configuration such as database URLs and secrets in environment variables. Before deployment, add structured logging, a request timeout policy, and a health check. The exact host is an operational choice; the API contract is the part your tests should pin down.

test_tasks.pyPython
from fastapi.testclient import TestClientfrom tasks import appclient = TestClient(app)def test_create_and_read_task():    created = client.post("/tasks", json={"title": "Write tests"})    assert created.status_code == 201    task_id = created.json()["id"]    fetched = client.get(f"/tasks/{task_id}")    assert fetched.status_code == 200    assert fetched.json()["title"] == "Write tests"def test_missing_task():    response = client.get("/tasks/999")    assert response.status_code == 404
LET IT CLICK

The ideas worth keeping.

  • Design the method, path, request shape, response shape, and status codes before implementation.
  • Validate at the HTTP boundary and keep domain rules in ordinary Python code.
  • Test the public API through TestClient, including missing resources and invalid bodies.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Add an update operation

Add PUT /tasks/{task_id} that accepts TaskIn, returns 404 for an unknown id, replaces the stored task while preserving its id, and returns the updated TaskOut.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
@app.put("/tasks/{task_id}", response_model=TaskOut)def update_task(task_id: int, task: TaskIn):    if task_id not in tasks:        raise HTTPException(status_code=404, detail="task not found")    updated = TaskOut(id=task_id, **task.model_dump())    tasks[task_id] = updated    return updated

The path identifies the resource, the body supplies the new values, and the response model keeps the public shape stable. The unknown-id branch is the same contract as the read route.

02

Test a validation failure

Write a test that posts an empty title and expects 422. Then add a test for a successful delete returning 204.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
def test_empty_title_is_rejected():    response = client.post("/tasks", json={"title": ""})    assert response.status_code == 422def test_delete_task():    created = client.post("/tasks", json={"title": "Temporary"})    task_id = created.json()["id"]    assert client.delete(f"/tasks/{task_id}").status_code == 204    assert client.get(f"/tasks/{task_id}").status_code == 404

A validation failure happens before the route body runs, so it should not create a task. A delete that succeeds returns no body, and a later read proves the resource is gone.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Which status code means a request body failed validation?
02Why use a response_model separate from the request model?
03What does TestClient exercise?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Add an update operation

Add PUT /tasks/{task_id} that accepts TaskIn, returns 404 for an unknown id, replaces the stored task while preserving its id, and returns the updated TaskOut.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.