FastAPI: From Basics to Production · Dependency Injection in Depth
Reusable Pagination and Filter Dependencies
Standardize list endpoints with dependency classes.
List endpoints share concerns: pagination, sorting and filtering. Encapsulate them in dependency classes or Pydantic models used with Depends() (or Annotated[Model, Query()] for query-parameter models, supported since FastAPI 0.115). Return a consistent envelope (items, total or next_cursor) and use cursor pagination for large or fast-changing tables.
from typing import Annotated, Generic, Literal, TypeVar
from fastapi import Query
from pydantic import BaseModel, Field
T = TypeVar("T")
class ListParams(BaseModel):
limit: int = Field(20, ge=1, le=100)
cursor: str | None = None
sort: Literal["created", "-created", "price", "-price"] = "-created"
class Page(BaseModel, Generic[T]):
items: list[T]
next_cursor: str | None
@app.get("/products")
async def list_products(params: Annotated[ListParams, Query()], db: DB) -> Page[ProductOut]:
rows, next_cursor = await products.page(db, params)
return Page(items=rows, next_cursor=next_cursor)
Common pitfalls
- Every endpoint inventing its own pagination parameters.
- Allowing arbitrary sort columns from user input.
- Expensive COUNT(*) on every request for huge tables.
Key points
- Reusable params models
- Consistent response envelopes
- Allow-listed sorting; cursor pagination at scale
Further reading: FastAPI: Query parameter models
Practice questions
Why restrict sort to a Literal of allowed values?
Answer
It prevents clients from sorting by arbitrary (possibly unindexed or sensitive) columns and avoids SQL injection risks when building ORDER BY clauses.
When is cursor pagination better than offset pagination?
Answer
For large or frequently changing datasets: it stays fast (no scanning skipped rows) and doesn't skip or duplicate items when rows are inserted.
Open in the app to mark lessons complete, take notes and track your progress.