Chapter 2: how FastAPI turns HTTP requests into typed Python calls. We cover path operations, every parameter source, routers, includes, and the small pieces that make routing feel ergonomic.
Path operations
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"id": user_id}
@app.get, @app.post, @app.put, @app.patch, @app.delete, @app.head, @app.options, @app.trace. There’s also @app.api_route(path, methods=["GET", "POST"]).
The decorator returns the original function. Test it directly without the framework:
result = await get_user(user_id=1)
Path parameters
@app.get("/items/{item_id}")
async def get_item(item_id: int):
...
{item_id}is captured from the URL.item_id: inttriggers validation:/items/abc→ 422.- Types:
int,float,str,bool,UUID, custom Pydantic-supported types.
For paths that contain slashes:
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
...
:path matches /. Useful for static-like resources.
Query parameters
Anything in the function signature without a path placeholder becomes a query parameter:
@app.get("/search")
async def search(q: str, limit: int = 20, offset: int = 0):
...
/search?q=foo&limit=10&offset=20.
Optional with None:
async def search(q: str | None = None):
...
For more control, use Query:
from fastapi import Query
async def search(
q: str | None = Query(None, min_length=2, max_length=100, regex="^[a-z]+$"),
tags: list[str] = Query(default_factory=list, description="filter tags"),
):
...
Query lets you set validation, examples, deprecation, alias.
Body parameters
Pydantic models become JSON bodies:
from pydantic import BaseModel
class UserIn(BaseModel):
email: str
name: str
@app.post("/users")
async def create_user(user: UserIn):
...
The framework reads the request body, parses JSON, validates against UserIn, passes the instance.
For multiple body params:
@app.put("/users/{id}")
async def update_user(id: int, user: UserIn, importance: int = Body(...)):
...
By default with multiple body params, FastAPI wraps them: {"user": {...}, "importance": 5}. Use Body(embed=True) on a single body param to wrap it the same way.
Header parameters
from fastapi import Header
@app.get("/items")
async def list_items(user_agent: str | None = Header(None)):
...
Underscores in Python become hyphens in HTTP. user_agent → User-Agent.
For convert_underscores=False if you really need underscores in the header.
Cookie parameters
from fastapi import Cookie
@app.get("/")
async def home(session: str | None = Cookie(None)):
...
Read named cookies. To set cookies: Response.set_cookie(...) or middleware.
Form data
from fastapi import Form
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
...
Requires python-multipart installed. Used for HTML forms / OAuth2 password flow.
File uploads
from fastapi import File, UploadFile
@app.post("/upload")
async def upload(file: UploadFile):
contents = await file.read()
return {"size": len(contents), "type": file.content_type, "name": file.filename}
UploadFile is preferred over bytes/File(...):
- It streams (doesn’t load whole file).
- It exposes file-like API.
- Works for big files.
For multiple files: files: list[UploadFile].
Routers
For larger apps:
# api/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
async def list_users(): ...
@router.get("/{id}")
async def get_user(id: int): ...
# main.py
from fastapi import FastAPI
from .api import users
app = FastAPI()
app.include_router(users.router)
Routers compose. They can have prefixes, dependencies, tags, responses.
Include with prefix and dependencies
app.include_router(
users.router,
prefix="/api/v1",
tags=["v1", "users"],
dependencies=[Depends(verify_api_key)],
)
Every route under this include requires the dependency. Useful for auth gates per router.
Nested routers
api = APIRouter(prefix="/api/v1")
api.include_router(users.router)
api.include_router(posts.router)
app.include_router(api)
Nest as deep as your URL hierarchy needs.
Mounts
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/admin", admin_app) # another ASGI app entirely
Mounts let you delegate paths to other ASGI apps. Useful for static files, admin dashboards, separate sub-apps.
Path operation parameters
Decorators take many options:
@app.post(
"/users",
response_model=UserOut,
status_code=201,
tags=["users"],
summary="Create a user",
description="Long description...",
response_description="The created user",
responses={400: {"description": "Bad input"}, 409: {"description": "Conflict"}},
deprecated=False,
operation_id="create_user",
)
async def create_user(...): ...
These shape OpenAPI. summary, description, tags, responses are visible in Swagger UI.
Status codes
from fastapi import status
@app.post("/users", status_code=status.HTTP_201_CREATED)
Default is 200 for non-DELETE; 200 for DELETE returning a body or 204 for no body.
The Request object
When you need raw access:
from fastapi import Request
@app.post("/raw")
async def raw(request: Request):
body = await request.body()
headers = dict(request.headers)
client_ip = request.client.host
...
For 95% of cases: declared parameters. Reach for Request only when you must.
Custom routing
For dynamic mount based on tenant:
from fastapi import APIRouter
from fastapi.routing import APIRoute
class TenantRoute(APIRoute):
def get_route_handler(self):
original = super().get_route_handler()
async def custom(request):
request.state.tenant = await load_tenant(request.headers["x-tenant"])
return await original(request)
return custom
router = APIRouter(route_class=TenantRoute)
Custom APIRoute subclasses let you wrap every handler. Powerful escape hatch.
URL generation
url = app.url_path_for("get_user", user_id=42)
# /users/42
Uses the function name (or name= arg on the decorator). Avoids hard-coded URL strings.
Request lifecycle (detailed)
For an incoming HTTP request:
1. ASGI server parses bytes; calls FastAPI.
2. CORS middleware (if applicable).
3. Custom middleware in registration order.
4. Routing: match path, method, prefix.
5. Path operation found; validate path params.
6. Dependency graph resolved (Chapter 5).
7. Body / query / header / cookie / form / file params read.
8. Handler coroutine awaited.
9. Return value passed to response_model serialization.
10. Response sent.
11. Background tasks fire (Chapter 9).
12. Middleware exits in reverse order.
Every step is hookable.
Common patterns
Versioned routes
v1 = APIRouter(prefix="/api/v1")
v2 = APIRouter(prefix="/api/v2")
v1.include_router(users_v1.router)
v2.include_router(users_v2.router)
app.include_router(v1)
app.include_router(v2)
Coexisting versions. See API Versioning .
Health endpoints
@app.get("/healthz", include_in_schema=False)
async def health():
return {"status": "ok"}
@app.get("/ready", include_in_schema=False)
async def ready(db: AsyncSession = Depends(get_db)):
await db.execute(text("SELECT 1"))
return {"status": "ready"}
Liveness (just up) vs readiness (deps reachable). include_in_schema=False keeps them out of OpenAPI.
Request ID propagation
@app.middleware("http")
async def request_id_mw(request: Request, call_next):
rid = request.headers.get("x-request-id") or str(uuid.uuid4())
request.state.request_id = rid
response = await call_next(request)
response.headers["x-request-id"] = rid
return response
Available throughout the request via request.state.request_id.
Common mistakes
1. Body params on GET
GET shouldn’t have a body. FastAPI allows it; intermediaries may strip it.
2. Catching all paths inadvertently
/{path:path} swallows everything; put it last.
3. Conflicting routers
Two routers with the same prefix mount on top of each other; the last one wins per path. Order matters.
4. Heavy work in middleware
Middleware runs every request. DB calls, big computation: avoid; use dependencies.
5. Ignoring name in routes
url_path_for needs route names. Default name is the function; rename routes get name=.
What’s next
Chapter 3 covers Pydantic models for input validation in depth.
Read this next
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .