Skip to main content

Query Parameters

Query parameters are the key-value pairs after ? in a URL: /items/?skip=0&limit=10&q=widget. FastAPI treats any function parameter that is not a path parameter and not a Pydantic model as a query parameter.

Learning Focus

By the end of this lesson you can: declare optional and required query parameters with type hints, apply constraints with Query, handle multi-value query params, and use Annotated for reusable parameter definitions.

Implicit Query Parameters

Any function parameter without a matching {name} in the path is a query parameter:

app/routers/items.py
from fastapi import APIRouter

router = APIRouter(prefix="/items", tags=["items"])

@router.get("/")
async def list_items(
skip: int = 0,
limit: int = 10,
q: str | None = None,
) -> dict:
return {"skip": skip, "limit": limit, "q": q}
test-query.sh
curl "http://localhost:8000/items/"
# → {"skip": 0, "limit": 10, "q": null}

curl "http://localhost:8000/items/?skip=5&limit=20&q=widget"
# → {"skip": 5, "limit": 20, "q": "widget"}

Required Query Parameters

Omit the default to make a query parameter required:

app/routers/search.py
from fastapi import APIRouter

router = APIRouter(prefix="/search", tags=["search"])

@router.get("/")
async def search(
q: str, # Required — no default
page: int = 1,
) -> dict:
return {"query": q, "page": page}

If q is omitted, FastAPI returns 422 Unprocessable Entity with field-level detail.

Advanced Constraints with Query

Use Annotated and Query for validation and OpenAPI metadata:

app/routers/items.py
from typing import Annotated
from fastapi import APIRouter, Query

router = APIRouter(prefix="/items", tags=["items"])

@router.get("/")
async def list_items(
skip: Annotated[int, Query(ge=0, description="Items to skip")] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
q: Annotated[
str | None,
Query(min_length=2, max_length=50, pattern=r"^[\w\s-]+$"),
] = None,
) -> dict:
return {"skip": skip, "limit": limit, "q": q}

Multi-Value Query Parameters

A single key can appear multiple times: /items/?tag=python&tag=async:

app/routers/items.py
from typing import Annotated
from fastapi import APIRouter, Query

@router.get("/by-tags")
async def filter_by_tags(
tag: Annotated[list[str], Query()] = [],
) -> dict:
return {"tags": tag}
curl "http://localhost:8000/items/by-tags?tag=python&tag=async"
# → {"tags": ["python", "async"]}

Reusable Parameter Definitions

app/dependencies/pagination.py
from typing import Annotated
from fastapi import Query

SkipParam = Annotated[int, Query(ge=0, description="Items to skip")]
LimitParam = Annotated[int, Query(ge=1, le=100, description="Max items")]
app/routers/products.py
from app.dependencies.pagination import SkipParam, LimitParam

@router.get("/")
async def list_products(skip: SkipParam = 0, limit: LimitParam = 10) -> dict:
return {"skip": skip, "limit": limit}

Bool Query Parameters

URL valuePython bool
true, 1, yes, onTrue
false, 0, no, offFalse
app/routers/items.py
@router.get("/active")
async def list_active(include_archived: bool = False) -> dict:
return {"include_archived": include_archived}

Common Pitfalls

PitfallCause / SymptomFix
Required query param causing 422Client forgot to include itAdd a default or document it in the API contract
List param receiving only one valueMissing list[str] type hintUse list[str] with Query() annotation
None not accepted but field optionalUsing str without | NoneUse str | None = None
Bool param rejecting "true"Raw string comparisonLet FastAPI coerce — use bool type hint
Pattern not shown in docspattern= missing in Query(...)Add pattern argument to Query call

Hands-On Practice

app/routers/movies.py
from typing import Annotated
from enum import Enum
from fastapi import APIRouter, Query

class Genre(str, Enum):
action = "action"
drama = "drama"
comedy = "comedy"

router = APIRouter(prefix="/movies", tags=["movies"])

_movies = [
{"id": 1, "title": "Fast Forward", "genre": "action", "rating": 7.5},
{"id": 2, "title": "The Quiet Storm", "genre": "drama", "rating": 8.2},
{"id": 3, "title": "Blazing Thunder", "genre": "action", "rating": 6.1},
]

@router.get("/")
async def search_movies(
genre: Genre | None = None,
min_rating: Annotated[float, Query(ge=0.0, le=10.0)] = 0.0,
q: Annotated[str | None, Query(min_length=2)] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=50)] = 10,
) -> list[dict]:
results = _movies
if genre:
results = [m for m in results if m["genre"] == genre.value]
if min_rating > 0:
results = [m for m in results if m["rating"] >= min_rating]
if q:
results = [m for m in results if q.lower() in m["title"].lower()]
return results[skip : skip + limit]
test-movies.sh
uvicorn app.routers.movies:router --reload --port 8004

curl "http://localhost:8004/movies/?genre=action&min_rating=7.0"
curl "http://localhost:8004/movies/?min_rating=11" -w "\n%{http_code}"
# → 422

What's Next