FastAPI request lifecycle: routing, dependencies, validation, and response models
Every request to an AI service travels the same path — routing, dependency injection, validation, handler, response model. Understanding that spine is what lets you put auth, tenancy, and validation where they belong.
The model is the interesting part, but it runs behind an HTTP service, and for Python that service is usually FastAPI. When someone asks 'how does a request actually enter your system?', they want the ordered path — and knowing it is what lets you put each concern (auth, tenancy, validation) at the right layer instead of scattering it through your handlers.
The path a request takes
Every request flows through the same stages, in order:
- Routing — FastAPI matches the request's method and path to a handler function.
- Dependencies — any Depends() the handler declares are resolved first and injected in (auth, tenant, DB session).
- Validation — the request body and parameters are parsed and validated against your Pydantic models; bad input is rejected with a 422 automatically, before your code runs.
- Handler — your function executes with clean, typed inputs.
- Response model — the return value is validated and serialised against the schema you declared, so the response contract is enforced, not hoped.
Dependencies: where cross-cutting concerns live
Depends() is FastAPI's dependency injection, and it's the single most useful thing to understand. A dependency is a function resolved before the handler and passed in as an argument — which makes it the natural home for everything that isn't the handler's core job: authentication (verify the token, load the current user), tenancy (resolve which tenant this request belongs to), database sessions, and rate limiting. Declare it once, reuse it across every route, and test it in isolation. Dependencies can depend on other dependencies, forming a small graph FastAPI resolves per request.
from fastapi import Depends, FastAPI, Header
app = FastAPI()
def get_tenant(authorization: str = Header(...)) -> Tenant:
return resolve_tenant(authorization) # resolved before the handler, injected in
@app.post('/query', response_model=Answer) # response validated against Answer
def query(body: Query, tenant: Tenant = Depends(get_tenant)) -> Answer:
return run_graph(body, tenant) # body already validated as QueryMiddleware vs dependencies
Both wrap your handler, but at different scopes. Middleware runs around every request, before routing even resolves — the right place for concerns that apply uniformly to all traffic: logging, CORS, timing, and stamping a request_id (which the structured-logging post shows you propagate downstream). Dependencies are per-route and have the route's context, and they can short-circuit with an error before the handler runs. The rule of thumb: middleware for the universal, dependencies for the route-specific.
Why this is the spine of the answer
For an AI service the handler is just where you call the graph; everything valuable around it is this lifecycle. Auth and tenant resolution live in dependencies, input and output contracts live in Pydantic models, and logging lives in middleware — each at its proper layer. Answer 'how does a request enter your service' with this ordered path (route → dependencies → validate → handle → response model) and you've shown you know where every concern belongs.
The handler is the small part. Routing, dependency injection, validation, and the response model are the spine — put auth in a dependency and the contract in a model, and the service stays clean as it grows.