Routes and Pydantic Models
Last phase you served a fixed response. A real API takes input - an ID in the URL, a filter in the query string, a JSON body on a POST. This phase covers all three, and shows you the part of FastAPI that does the most work for the least code: validation driven by type hints.
Keep the server running with --reload. Edit main.py and watch /docs update
as you save.
Path parameters: data in the URL
A path parameter is a piece of the URL that changes - the 42 in /notes/42.
You declare it with curly braces in the route and as an argument to the
function:
=
return
Notice note_id: int. That type hint isn't decoration. Visit
http://127.0.0.1:8000/notes/42 and you get:
FastAPI converted the string "42" from the URL into an actual integer. Now try
http://127.0.0.1:8000/notes/banana. Instead of crashing, you get a clean
422 response:
You wrote zero validation code. The type hint did it. This is the core idea of FastAPI - you describe the shape of your data with normal Python types, and the framework enforces it.
Query parameters: data after the ?
A query parameter is the ?limit=5 part of a URL. FastAPI gives you these for
free: any function argument that isn't in the path becomes a query parameter.
return
Two things to read carefully:
limit: int = 10has a default, so it's optional. Visit/notesand you getlimit: 10. Visit/notes?limit=3and you get3. Pass/notes?limit=abcand you get a 422 - theinthint is doing its job again.q: str | None = Nonemeans "an optional string". It's there when you want a search term and absent otherwise.
Path vs query, side by side:
| Path param | Query param | |
|---|---|---|
| Lives in | the URL path: /notes/42 |
after the ?: /notes?limit=5 |
| Declared by | {name} in the route |
a function arg not in the path |
| Optional? | no - it's part of the address | yes, if it has a default |
| Good for | identifying which resource | filtering, sorting, paging |
Request bodies: a Pydantic model
GET requests carry data in the URL. But to create a note you need to send a chunk of JSON - a title and some content - in the request body. For that you define the expected shape as a Pydantic model.
Add this to the top of main.py:
:
:
: = False
A BaseModel subclass is a description of valid input. title and content
are required strings; pinned is an optional boolean that defaults to False.
Now write a POST endpoint that takes one:
return
Because note is typed as your model, FastAPI knows the data comes from the
request body. It reads the incoming JSON, checks it against NoteIn, and hands
you a fully-typed note object - note.title, note.pinned, with editor
autocomplete and everything.
Try it from the docs
Go to http://127.0.0.1:8000/docs. The POST /notes endpoint is there, and so
is a schema showing exactly which fields it expects - FastAPI generated that from
your model. Click it, hit Try it out, and send this body:
You'll get back the parsed data plus the title length. Now break it on purpose -
send a body with title missing:
The response is a 422 that points at the exact problem:
That error message is generated from your model. You didn't write a single
if "title" not in data check.
Why this matters
Most of the validation code in a hand-rolled API is checking that fields exist and have the right type. Pydantic turns that whole category of code into a class definition. You declare the shape once, and every endpoint that uses the model gets the checks, the 422 errors, and the docs.
Where we are
You can now take input three ways - path, query, and body - and FastAPI validates all of it from your type hints. The data still vanishes the moment the request ends, though; nothing is stored. Next phase we give the notes a place to live and wire up all four CRUD operations.