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.