What you'll learn: how a FastAPI service is put together, using the capstone's
create_appas the example: routes declared with typed request and response models, the documentation that comes for free, what happens to a request between the network and your function, and howDependscompares with Spring beans and constructor injection.
At a glance
Figure 1. How create_app assembles the service
How to read this: everything the service needs is wired inside one function that takes its settings and services as arguments, so a test can build its own copy.
- A route is a plain function whose parameter types describe the request, and whose return type or
response_modeldescribes the reply. - FastAPI validates the request before your function runs and generates the API documentation from the same declarations.
Dependsis a lighter cousin of Spring injection: you name the function that provides a value, and it runs per request.- The capstone passes shared objects through
create_apparguments andapp.stateinstead ofDepends, which is a valid choice too. - Tests swap a dependency with
app.dependency_overrides, with no change to the route.
Scenario
Composite scenario, not a real company. Lumen Industrial Supply, the fictional distributor, wants the pilot's discovery feature behind an HTTP API so its portal, written in React, can call it. The platform team is used to Spring Boot controllers and wants to know what the Python service looks like, how clients learn its contract, and how to test a route without a real identity provider. All names here are illustrative.
What you will be able to do
- Read a FastAPI route and say how each parameter is read from the request.
- Explain, in a table, how
Dependsdiffers from Spring bean injection in lifetime and wiring. - Build a small service with a typed response, a dependency and a test that replaces the dependency.
- Describe the order in which middleware, validation, dependencies and your function run.
Prerequisites
Python for Java and Node engineers and Dataclasses and Pydantic come first: FastAPI uses Pydantic models as its request and response contracts. The next lessons add async and retries and errors, configuration and logging.
Core ideas
A route is a decorated function
Verified: capstone/fde-product-discovery-copilot/app/main.py
@api.post("/discovery", response_model=DiscoveryResponse)
def discovery(
body: DiscoveryRequest,
request: Request,
response: Response,
authorization: Annotated[str | None, Header()] = None,
idempotency_key: Annotated[str | None, Header(max_length=100)] = None,
) -> dict[str, object]:
identity = who(authorization)
permissions.require(identity, "discovery")
A decorator is a callable applied to the function beneath it, and what it returns takes the function's name. Here @api.post(...) registers the function as a route. The FastAPI body documentation lists what a typed parameter buys: it will "read the body of the request as JSON", "convert the corresponding types", "validate the data" and "generate JSON Schema definitions for your model".
| Parameter in the capstone | Where FastAPI reads it from |
|---|---|
body: DiscoveryRequest |
The JSON body, validated by the Pydantic model |
request: Request, response: Response |
The raw request and response objects, for headers and state |
authorization: Annotated[str | None, Header()] |
The Authorization header, optional |
idempotency_key: Annotated[str | None, Header(max_length=100)] |
The Idempotency-Key header: FastAPI turns underscores into hyphens by default |
response_model=DiscoveryResponse tells FastAPI to validate and filter what the function returns. The docs say it will "convert and filter the output data" to that type, so a field that is not in the model never leaves the service.
What happens to a request
Predict first. A client sends POST /api/v1/discovery with a valid token and the JSON body {"request": "hi"}. The model says request needs at least 3 characters. What status code comes back, what is in the body, and does run_discovery run?
Answer
Status 422. The body is the capstone's single error shape, {"error": {"code": "validation_error", "message": "Invalid input: request", "trace_id": ...}}, and the trace id matches the X-Trace-Id response header. The route function never starts, so run_discovery is not called. The test below runs exactly this request.
Verified: capstone/fde-product-discovery-copilot/lessons/M1-L3/test_m1_l3_service.py
def test_capstone_rejects_a_two_character_request_with_its_error_shape():
app = create_app(Settings())
r = TestClient(app).post("/api/v1/discovery", json={"request": "hi"}, headers=bearer_for_capstone())
assert r.status_code == 422
body = r.json()["error"]
assert body["code"] == "validation_error" and body["message"] == "Invalid input: request"
assert body["trace_id"] == r.headers["X-Trace-Id"]
Figure 2. Request lifecycle of POST /api/v1/discovery
How to read this: your function sits in the middle of the sequence, and a request that fails validation reaches the error handler instead of it while still passing through the middleware on the way out.
The middleware, in the FastAPI docs' words, "works with every request before it is processed by any specific path operation", and with every response before it is returned. The capstone's trace-id middleware uses that to stamp every reply.
Verified: capstone/fde-product-discovery-copilot/app/main.py