Declare the parameter name inside curly braces in the path string and add a matching argument to the handler function. FastAPI parses and type-coerces it automatically.
@app.get("/items/{item_id}")
async def get_item(item_id: int):
return {"item_id": item_id}
# GET /items/42 → {"item_id": 42} (integer, not string)
If the value can't be coerced to the declared type, FastAPI returns 422.
Rule of thumb: always type-annotate path parameters so FastAPI validates the URL segment before the handler runs.
Declare a function parameter with a type annotation and no default value. FastAPI treats any simple type not in the path template as a query parameter.
@app.get("/items")
async def search_items(q: str):
return {"q": q}
# GET /items?q=laptop → {"q": "laptop"}
# GET /items → 422 (required param missing)
Rule of thumb: no default → required query param; add = None or = value
to make it optional.
Assign a default in the function signature. FastAPI uses it when the client omits the parameter.
@app.get("/items")
async def list_items(
page: int = 1,
size: int = 20,
active: bool = True,
):
return {"page": page, "size": size, "active": active}
# GET /items → {"page": 1, "size": 20, "active": true}
# GET /items?page=3 → {"page": 3, "size": 20, "active": true}
Rule of thumb: choose defaults that represent the most common use case so clients don't need to pass boilerplate on every request.
FastAPI accepts a flexible range of truthy/falsy string values and converts them
to Python bool:
- Truthy:
"1","true","on","yes"(case-insensitive) - Falsy:
"0","false","off","no"(case-insensitive)
@app.get("/items")
async def list_items(active: bool = True):
...
# GET /items?active=false → active = False
# GET /items?active=0 → active = False
# GET /items?active=yes → active = True
Any other value triggers a 422 validation error.
Rule of thumb: use bool for feature flags/filters in query params — FastAPI's
flexible string-to-bool coercion covers all common client conventions.
FastAPI gives priority to the path parameter. A parameter name that appears
in the route template /{name} is always a path parameter; you cannot also read
it from the query string with the same name in the same handler.
@app.get("/items/{id}")
async def get_item(id: int, q: str | None = None):
# id → from path, q → from query
...
If you genuinely need both a path and query parameter with the same name, use an
alias: Query(alias="id") — though this is a design smell.
Rule of thumb: keep path and query parameter names distinct; collision means you should rethink the route design.
Path() is FastAPI's parameter helper for adding metadata and validation
constraints to path parameters while keeping the type annotation clean.
from typing import Annotated
from fastapi import Path
@app.get("/items/{item_id}")
async def get_item(
item_id: Annotated[int, Path(title="Item ID", ge=1, le=999_999)],
):
return {"id": item_id}
Constraints (ge, le, gt, lt) are enforced at validation time and
reflected in the OpenAPI schema. You can also pass description, example, and
deprecated.
Rule of thumb: use Path() whenever a path parameter has a meaningful range,
description, or example worth documenting in the schema.
Query() adds string constraints (min_length, max_length, pattern),
numeric bounds, metadata (title, description, example), and the ability
to collect multi-value parameters.
from typing import Annotated
from fastapi import Query
@app.get("/search")
async def search(
q: Annotated[str, Query(
min_length=3,
max_length=100,
description="Search term",
example="fastapi",
)],
sort: Annotated[str, Query(pattern=r"^(asc|desc)$")] = "asc",
):
...
Rule of thumb: add Query() the moment you need any constraint or documentation
on a query parameter — it's zero runtime cost and improves the schema immediately.
Annotate with list[T] and wrap with Query():
from typing import Annotated
from fastapi import Query
@app.get("/items")
async def filter_items(
tags: Annotated[list[str], Query()] = [],
):
return {"tags": tags}
# GET /items?tags=python&tags=web → {"tags": ["python", "web"]}
Without Query(), list[str] would be interpreted as a JSON body parameter.
An empty list default (= []) means the param is optional; use = Query(min_length=1)
on the list if at least one tag is required.
Rule of thumb: list[T] + Query() = multi-value param; the client repeats
the key multiple times in the query string.
Pass alias= to Query() or Path(). FastAPI reads the value from the alias
key in the request but binds it to the Python name in the handler.
from typing import Annotated
from fastapi import Query
@app.get("/items")
async def list_items(
item_query: Annotated[str | None, Query(alias="item-query")] = None,
):
return {"query": item_query}
# GET /items?item-query=foo → item_query = "foo"
This is useful when the URL convention requires hyphens (which are invalid Python identifiers) or when you're preserving backward-compatible parameter names.
Rule of thumb: use alias to bridge the gap between URL naming conventions
(hyphens) and Python naming conventions (underscores).
Pass deprecated=True to Query() or Path(). The parameter still works
at runtime — it's marked deprecated in the generated schema only.
from typing import Annotated
from fastapi import Query
@app.get("/items")
async def list_items(
q: Annotated[str | None, Query()] = None,
search: Annotated[str | None, Query(deprecated=True)] = None, # old alias
):
effective_q = q or search
...
Swagger UI renders deprecated parameters with a strikethrough and a warning badge.
Rule of thumb: mark old parameter names deprecated rather than removing them immediately — gives clients a migration window while keeping the schema honest.
| Constraint | Meaning |
|---|---|
ge=n |
greater than or equal (>=) |
gt=n |
strictly greater than (>) |
le=n |
less than or equal (<=) |
lt=n |
strictly less than (<) |
multiple_of=n |
value must be a multiple of n |
from typing import Annotated
from fastapi import Query, Path
@app.get("/items/{item_id}")
async def get_item(
item_id: Annotated[int, Path(ge=1)], # ID must be positive
page: Annotated[int, Query(ge=1, le=100)] = 1, # page 1-100
price_min: Annotated[float, Query(gt=0)] = 0.0,
):
...
Constraints are reflected in the OpenAPI JSON Schema properties so client validators can enforce them before the request is sent.
Rule of thumb: apply ge=1 to all ID path parameters — negative or zero IDs
are almost always bugs.
| Constraint | Meaning |
|---|---|
min_length=n |
minimum string length |
max_length=n |
maximum string length |
pattern=r"..." |
regex the value must match |
from typing import Annotated
from fastapi import Query
@app.get("/users")
async def search_users(
username: Annotated[str, Query(min_length=3, max_length=50, pattern=r"^\w+$")],
):
...
# GET /users?username=al → 422 (min_length=3)
# GET /users?username=al!ce → 422 (pattern)
Rule of thumb: always set max_length on free-text query params to prevent
accidental DoS from clients sending huge query strings.
Pass include_in_schema=False to Query(). The parameter still works at runtime
but won't appear in /openapi.json or the docs.
from typing import Annotated
from fastapi import Query
@app.get("/items")
async def list_items(
q: str | None = None,
_internal_trace_id: Annotated[str | None, Query(include_in_schema=False)] = None,
):
...
Rule of thumb: use include_in_schema=False for internal/infra params
(tracing IDs, A/B test flags) that aren't part of the public API contract.
Path parameters are for identifying a specific resource:
GET /users/{user_id} # identifies a specific user
GET /orders/{order_id}/items # items within a specific order
Query parameters are for filtering, sorting, searching or pagination of a collection:
GET /users?role=admin&page=2 # filter users by role, paginate
GET /orders?status=pending # filter orders
Mixing them up leads to ugly URLs like /users?id=42 (should be /users/42)
or /users/admin for a filter (should be /users?role=admin).
Rule of thumb: if removing the identifier would leave a meaningless URL
(/users/ is just a list), it belongs in the path; if it's a filter, it's a query param.
More Routing & Parameters interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.