Path & Query Parameters¶
URLs carry data in two ways:
https://api.example.com/users/42?lang=en&active=true
└─┬─┘ └─────────┬─────────┘
path param query params
Path parameters¶
Use {name} in the route. Python's type hint tells FastAPI how to parse it.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"id": user_id, "name": f"User #{user_id}"}
Run with uvicorn main:app --reload. Then:
http://127.0.0.1:8000/users/42→{"id": 42, "name": "User #42"}http://127.0.0.1:8000/users/abc→ automatic 422 error (not an int!)
That's automatic validation — free.
How it executes¶
When you hit /users/42:
- FastAPI matches
/users/{user_id}against the URL. - It extracts
"42"as a string from the URL. - It sees the type hint
user_id: int— converts"42"→42. - If conversion fails (
/users/abc), it returns HTTP 422 automatically — your function isn't even called. - Function runs, return dict → JSON response.
Multiple path params¶
@app.get("/users/{user_id}/posts/{post_id}")
def get_post(user_id: int, post_id: int):
return {"user": user_id, "post": post_id}
/users/3/posts/77 → {"user": 3, "post": 77}
Path parameter validation — Path(...)¶
For finer rules (min, max, regex):
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(..., ge=1, le=1000, description="Item ID, 1-1000")
):
return {"item_id": item_id}
...means "required" (Ellipsis convention).ge=1, le=1000— value must be1 ≤ id ≤ 1000.description=...shows up in the Swagger docs.
/items/500 → ok.
/items/9999 → 422: ensure this value is less than or equal to 1000.
Query parameters¶
Anything you put as a function argument that's not in the URL path becomes a query parameter.
@app.get("/products")
def list_products(category: str = "all", limit: int = 10):
return {
"category": category,
"limit": limit,
"products": [] # imagine a DB query here
}
/products→category="all",limit=10(defaults)./products?category=books→category="books",limit=10./products?category=books&limit=25→ both set.
Order doesn't matter. ?limit=25&category=books works the same.
Optional query parameters¶
Use | None = None (Python 3.10+ union syntax) or Optional:
@app.get("/search")
def search(q: str | None = None, page: int = 1):
if q is None:
return {"error": "missing query"}
return {"query": q, "page": page, "results": []}
/search → {"error": "missing query"}
/search?q=fastapi → {"query": "fastapi", "page": 1, ...}
Required query parameters¶
A parameter with no default value becomes required:
@app.get("/orders/{user_id}")
def get_orders(user_id: int, status: str): # status has no default → required
return {"user": user_id, "status": status}
/orders/5 → 422: field required.
/orders/5?status=paid → ok.
Query parameter validation — Query(...)¶
Same idea as Path() but for query params:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search(
q: str = Query(..., min_length=3, max_length=50,
description="search term", example="fastapi"),
limit: int = Query(10, ge=1, le=100),
):
return {"q": q, "limit": limit}
/search?q=hi → 422: string too short.
Boolean query parameters¶
Accepted values for True: true, True, 1, yes, on.
For False: false, False, 0, no, off.
List query parameters¶
?tags=python&tags=ml&tags=ai:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def list_items(tags: list[str] = Query(default=[])):
return {"tags": tags}
/items?tags=python&tags=ml → {"tags": ["python", "ml"]}
Combining path + query¶
@app.get("/users/{user_id}/orders")
def user_orders(user_id: int, status: str = "all", limit: int = 20):
return {
"user": user_id,
"status": status,
"limit": limit,
}
/users/5/orders?status=paid&limit=50
A complete example¶
# main.py
from fastapi import FastAPI, Path, Query
app = FastAPI(title="Product API")
@app.get("/")
def root():
return {"message": "Product API — try /products"}
@app.get("/products")
def list_products(
category: str | None = Query(None, description="filter by category"),
limit: int = Query(10, ge=1, le=100),
sort: str = Query("name", regex="^(name|price|date)$"),
):
return {
"category": category,
"limit": limit,
"sort": sort,
"products": ["TBD — wire up the database"]
}
@app.get("/products/{product_id}")
def get_product(
product_id: int = Path(..., ge=1, description="positive integer"),
):
return {"product_id": product_id, "name": f"Product #{product_id}"}
Run it:
Then try: - http://127.0.0.1:8000/products - http://127.0.0.1:8000/products?category=books&limit=5&sort=price - http://127.0.0.1:8000/products/42 - http://127.0.0.1:8000/docs — interactive
Common pitfalls¶
- ❗ Forgetting the type hint —
def get_user(user_id):(no type) treats it as a string with no validation. Always add: int,: str, etc. - ❗
/users/{user_id}but function param is named differently — names must match exactly. - ❗ Path params with default values — illegal. Path params are always required.
- ❗ Mixing path and query order — order doesn't matter for function args, but path must match the URL exactly.
What's next¶
Practice¶
What does this print?
Expected: 42
Use type hint int so /items/abc returns 422 (not crashes the handler)
Expected: True
Quiz — Quick check¶
What you remember
Q1. What's the difference between path params and query params?
- Path params are part of the URL path (
/items/{id}); query params come after?(/items?id=42) - No difference
- Path params are GET, query params are POST
- Path params are optional
Why: Different positions in the URL. Path = required, hierarchical. Query = optional, filters/options. FastAPI handles both automatically based on whether the name appears in the route string.
Q2. What happens when /items/{item_id} is hit with /items/abc and the type is int?
- Returns the string "abc"
- Returns 422 Unprocessable Entity with a clear validation error
- Crashes the server
- Returns 200 with empty data
Why: FastAPI validates path params against the type hint. Conversion failure produces a 422 with a JSON body explaining what went wrong. Clean error responses for free.
Q3. How do you make a query parameter optional?
- Give it a default value:
def f(q: str = None)ordef f(q: str | None = None) - Add
optional=True - Use
Optional[str]only - Wrap in a list
Why: Default value = optional. Without a default, FastAPI requires the param and returns 422 if missing. Use
None(with Union type) for "may be omitted".
Common doubts¶
How do I validate that a query param is a positive integer?
Use Query() with constraints: from fastapi import Query; def f(q: int = Query(gt=0)). Adds the constraint to validation AND to the OpenAPI schema. Returns 422 for negative or zero values.
Can I have multiple values for one query param?
Yes — def f(ids: list[int] = Query()). Then /items?ids=1&ids=2&ids=3 gives you [1, 2, 3]. Modern Python type syntax works (list[int]); older requires List[int] from typing.
Path params order — does it matter?
Yes — function arg names must match the names in the route string. Order in the function signature doesn't matter for path params (FastAPI matches by name) but does for the URL itself.