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:

Valid vs. invalid path parameter
click Run to see this pane's output

That 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}
Query parameters from the URI
click Run to see this pane's output

That 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}
Constraints beyond just type
click Run to see this pane's output

Query(default=10, gt=0, le=100) means: default to 10 if omitted, reject anything not strictly greater than 0, reject anything above 100limit 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"}
/agents/{agent_id} defined first — wrong
click Run to see this pane's output

That request matches /agents/{agent_id} firstme 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}
/agents/me defined first — fixed
click Run to see this pane's output

Same 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.

Check your understanding
1/5

In @app.get("/agents/{agent_id}") def get_agent(agent_id: int):, what does the int type hint actually do?