Handbooks / FastAPI / Chapter 1
Routing and Pydantic Models
7 pages · ~5 min✓ Reviewed
Start here. Next up: Dependency Injection.
Path Operations and the Mental Model
1. What a Path Operation Is
- Path operation = method decorator + path + function
@app.get('/items')binds a URL and verb to a function- Function name and docstring feed the docs
- Type hints drive everything: source, conversion, validation, docs
python
@app.get("/items/{item_id}") def read_item(item_id: int, q: str | None = None): """Fetch one item.""" return {"item_id": item_id, "q": q}
item_id from path, q from query
2. Where Each Argument Comes From
| Signature clue | Source |
|---|---|
| Name in the path template | Path param |
Plain scalar type (int, str, bool) | Query param |
| Pydantic model | JSON body |
3. def vs async def, and Route Order
async defonly when you await async libraries- Plain
defruns in a threadpool, so blocking calls do not freeze the loop async defwith blocking code stalls the whole event loop
python
@app.get("/users/me") def me(): ... @app.get("/users/{user_id}") def user(user_id: int): ...
fixed route first
Path order matters
Declare /users/me before /users/{user_id}, or 'me' is parsed as an id and fails validation.
Path and Query Parameters
1. Path vs Query
| Param | Declared as | Required? |
|---|---|---|
| Path | name inside {item_id} | always |
| Query | argument not in path, no default | yes |
| Query + default | limit: int = 10 | no |
| Optional query | q: str | None = None | no |
item_id: intparses the path;/items/abcreturns 422boolaccepts true/1/on/yes and arrives as a realbool
2. Constraints
Path(ge=1, le=1000)bounds a number
Request Bodies with Pydantic v2
1. Body = a BaseModel
- Param typed as a BaseModel subclass → read as JSON body
- FastAPI validates it and hands you an
Iteminstance - No default → required; None-able still needs
= None
python
class Item(BaseModel): name: str price: float tags: list[str] = [] note: str | None = None @app.post("/items") async def create(item: Item): ...
name and price required
2. Pydantic v2 names
| v1 (gone) | v2 |
|---|---|
.dict() | model_dump() |
parse_obj() | model_validate() |
| PATCH partial | model_dump(exclude_unset=True) |
3. Mixing sources
- Path + query + body in one function works
Validation with Field and Errors
1. Field() Constraints
Field()adds constraints and docs metadata inside a model
python
class Item(BaseModel): name: str = Field(min_length=1, max_length=50) price: float = Field(gt=0, description='USD')
| Type | Constraints |
|---|---|
| Numbers | gt ge lt le |
| Strings | min_length max_length pattern |
| Docs | examples= description= |
2. Custom Checks (v2)
@field_validator('name')on a classmethod: one field@model_validator(mode='after'): cross-field rules- v1
@validatoris gone
response_model and Status Codes
1. Filtering the Output
response_model=UserOutfilters and validates the returned data- Secrets like password never leave, even if you return the full object
- Pattern: UserIn (with password) and UserOut (without)
- Return annotation
-> UserOutdoes the same job
python
class UserIn(BaseModel): name: str password: str class UserOut(BaseModel): name: str @app.post("/users", status_code=status.HTTP_201_CREATED) async def create(u: UserIn) -> UserOut: return u
password is dropped from the response
2. Shape and Status
| Option | Effect |
|---|---|
response_model_exclude_unset=True | drops unset defaults |
response_model_exclude_none=True | drops nulls |
response_model=list[Item] | a collection |
Automatic OpenAPI Docs and Cheat Sheet
1. Docs From Your Code
/docs= Swagger UI; Try it out sends real calls/redoc= ReDoc, read-only reference/openapi.json= the raw schema- Built from type hints, models and decorator arguments
python
@app.get("/items", tags=["items"], summary="List items", response_description="All items", deprecated=True) def list_items(): """Docstring becomes the description.""" class Item(BaseModel): price: float = Field(gt=0, examples=[9.99]) model_config = {"json_schema_extra": {"examples": [{"price": 9.99}]}}
decorator args, docstring, examples in Field / model_config
2. Where Each Argument Comes From
| Source | Rule | Example |
|---|---|---|
| Path | name in path template | /items/{id} |
| Query | scalar, not in path | limit: int = 10 |
Check yourself
Broken code: @app.get('/users/{user_id}') with user_id: int is declared before @app.get('/users/me'). What does GET /users/me return?
- 422:
mematches the first route and fails int parsing locis['path','user_id']- Fixed code:
@app.get('/users/me')declared before@app.get('/users/{user_id}')
Broken code: def items(tags: list[str] = []) on a GET. What is tags for ?tags=a&tags=b?
[]: withoutQuery()a list is read from the body, so the query string is ignored- Fixed code:
tags: Annotated[list[str], Query()] = []
Broken code: note: str | None with no default in a v2 model. What does POST body {} give?
- 422: still required in v2,
locis['body','note'] - Fixed code:
note: str | None = None