Routes, path/query parameters, and validation
Path parameters — part of the URI itself
A {"{...}"} segment in a route’s path becomes a function parameter,
automatically extracted from the actual URI a request came in on:
from fastapi import FastAPI
app = FastAPI()
@app.get("/agents/{agent_id}")
def get_agent(agent_id: int):
return {"agent_id": agent_id, "name": "research_agent"}A request to /agents/42 calls get_agent(agent_id=42) — as an actual
int, already converted, not the string "42". This is
the same automatic-validation mechanism from the previous concept,
now applied to a path segment: the type hint isn’t just documentation
here, it’s a real parsing step.
Requesting /agents/not-a-number — something that can’t be parsed as an
int — never reaches get_agent at all:
click Run to see this pane's outputThat second response is automatic — HTTP status 422, a structured
error body, and get_agent never actually running. No manual
isinstance check, no try/except needed for this case at all.
Query parameters — everything else in the function signature
Any function parameter not named in the path becomes a query
parameter instead — read from the URI’s ?key=value portion:
@app.get("/agents")
def list_agents(model: str = None, limit: int = 10):
return {"model_filter": model, "limit": limit}click Run to see this pane's outputThat request calls list_agents(model="claude-sonnet", limit=5). A
default value (= None, = 10) makes the parameter optional — omitting
it from the URI just uses the default, exactly like
a regular function’s default argument.
Query() and Path() — validation beyond just the type
A type hint alone only checks shape (is this an int). Query() and
Path() add real constraints — a minimum, a maximum, a length limit —
directly in the function signature:
from fastapi import FastAPI, Query, Path
app = FastAPI()
@app.get("/agents")
def list_agents(limit: int = Query(default=10, gt=0, le=100)):
return {"limit": limit}
@app.get("/agents/{agent_id}")
def get_agent(agent_id: int = Path(gt=0)):
return {"agent_id": agent_id}click Run to see this pane's outputQuery(default=10, gt=0, le=100) means: default to 10 if omitted,
reject anything not strictly greater than 0, reject anything above
100 — limit never even reaching list_agents in the first case
above. Path(gt=0) does the same for a path parameter — the second
case — refusing an agent_id of 0 or negative, even though it’s
already a valid int.
The route-ordering gotcha
Routes are matched top to bottom, and the first matching route wins — worth being careful with, since a dynamic path segment matches almost anything:
@app.get("/agents/{agent_id}")
def get_agent(agent_id: str):
return {"agent_id": agent_id}
@app.get("/agents/me")
def get_current_agent():
return {"agent_id": "current-user's-agent"}click Run to see this pane's outputThat request matches /agents/{agent_id} first — me is a perfectly
valid string for agent_id — so get_current_agent never actually
runs; get_agent(agent_id="me") handles it instead. The fix is ordering
the more specific, static route before the dynamic one:
@app.get("/agents/me")
def get_current_agent():
return {"agent_id": "current-user's-agent"}
@app.get("/agents/{agent_id}")
def get_agent(agent_id: str):
return {"agent_id": agent_id}click Run to see this pane's outputSame request, same two routes, just reordered — now the specific route actually gets a chance to match before the dynamic one swallows it. This is the same top-to-bottom, first-match-wins principle from except clause ordering, applied to routes instead of exception types: list the more specific case first, or it never gets a chance to match.
In @app.get("/agents/{agent_id}") def get_agent(agent_id: int):, what does the int type hint actually do?