Query Parameters — Filtering, Pagination and Optional Values

Query parameters are the key-value pairs after the ? in a URL — /posts?status=published&page=2&limit=10. Unlike path parameters, they are optional by default (when a default value is provided) and can appear in any order. FastAPI function parameters that are not path parameters and not Pydantic models are automatically treated as query parameters. They work exactly like path parameters in terms of type validation and Query() constraints — the only difference is where in the URL they appear.

Declaring Query Parameters

from fastapi import FastAPI, Query
from typing import Annotated, Literal

app = FastAPI()

# ── Simple query parameters ───────────────────────────────────────────────────
@app.get("/posts")
def list_posts(
    page:    int  = 1,           # optional, default 1
    limit:   int  = 10,          # optional, default 10
    search:  str  = "",          # optional, default empty string
    published_only: bool = True, # optional, default True
):
    return {
        "page": page,
        "limit": limit,
        "search": search,
        "published_only": published_only,
    }

# GET /posts                                  → page=1, limit=10, search="", published_only=True
# GET /posts?page=2&limit=20                  → page=2, limit=20
# GET /posts?search=fastapi&published_only=false → search="fastapi", published_only=False
# GET /posts?page=abc                         → 422 (abc not int)
Note: FastAPI automatically parses boolean query parameters from common string representations: true, 1, yes, onTrue; false, 0, no, offFalse. This means ?published_only=true, ?published_only=True, ?published_only=1, and ?published_only=yes all work. The same case-insensitive parsing applies to Literal string values — ?order=ASC will not match Literal["asc"] though, since string Literals are case-sensitive.
Tip: For list query parameters (accepting multiple values), annotate the type as list[str] or list[int] with Query(). The client sends multiple values as repeated parameters: ?tags=python&tags=fastapi&tags=api — FastAPI collects them into a list. Without the Query() wrapper, FastAPI will only see the last value. With it: tags: Annotated[list[str], Query()] = [] gives you ["python", "fastapi", "api"].
Warning: Required query parameters (no default value) are unusual in REST APIs — clients that do not know about them will get confusing 422 errors. Prefer optional query parameters with sensible defaults for list/filter endpoints. Only make a query parameter required when it is genuinely impossible to proceed without it, and document it clearly. For parameters that must be provided for security reasons (like an API key), use a header instead.

Query() Constraints and Documentation

from fastapi import FastAPI, Query
from typing import Annotated, Literal

app = FastAPI()

@app.get("/posts")
def list_posts(
    page: Annotated[int, Query(
        ge          = 1,
        description = "Page number (1-indexed)",
        example     = 1,
    )] = 1,

    limit: Annotated[int, Query(
        ge          = 1,
        le          = 100,
        description = "Number of results per page (max 100)",
        example     = 10,
    )] = 10,

    search: Annotated[str | None, Query(
        max_length  = 200,
        description = "Search term to filter posts by title or body",
    )] = None,

    sort_by: Annotated[
        Literal["created_at", "view_count", "title"],
        Query(description="Field to sort results by")
    ] = "created_at",

    order: Annotated[
        Literal["asc", "desc"],
        Query(description="Sort direction")
    ] = "desc",

    tags: Annotated[list[str], Query(
        description = "Filter by tags (can specify multiple)",
        example     = ["python", "fastapi"],
    )] = [],
):
    return {
        "page": page, "limit": limit, "search": search,
        "sort_by": sort_by, "order": order, "tags": tags
    }

# GET /posts?page=2&limit=20&sort_by=view_count&order=desc&tags=python&tags=fastapi

Optional Query Parameters with None

from fastapi import FastAPI
from datetime import date

app = FastAPI()

@app.get("/posts")
def list_posts(
    author_id:   int | None  = None,   # filter by author if provided
    from_date:   date | None = None,   # filter by date range if provided
    to_date:     date | None = None,
    status:      str | None  = None,
):
    # Build query dynamically based on what was provided
    query = db.query(Post)
    if author_id is not None:
        query = query.filter(Post.author_id == author_id)
    if from_date:
        query = query.filter(Post.created_at >= from_date)
    if to_date:
        query = query.filter(Post.created_at <= to_date)
    if status:
        query = query.filter(Post.status == status)
    return query.all()

# GET /posts?author_id=5               → only posts by author 5
# GET /posts?from_date=2025-01-01&to_date=2025-12-31 → date range
# GET /posts                           → all posts

Common Mistakes

Mistake 1 — List query parameter without Query()

❌ Wrong — FastAPI only sees the last value:

def list_posts(tags: list[str] = []):
    # GET /posts?tags=python&tags=fastapi → tags=["fastapi"] (only last!)

✅ Correct — wrap with Query():

def list_posts(tags: Annotated[list[str], Query()] = []):
    # GET /posts?tags=python&tags=fastapi → tags=["python", "fastapi"] ✓

Mistake 2 — Not limiting page size (DoS risk)

❌ Wrong — client can request millions of rows:

def list_posts(limit: int = 10):   # GET /posts?limit=1000000 → loads 1M rows!

✅ Correct — add maximum constraint:

def list_posts(limit: Annotated[int, Query(ge=1, le=100)] = 10):   # ✓ max 100

Mistake 3 — Unvalidated sort_by field (SQL injection risk)

❌ Wrong — user-controlled column name in ORDER BY:

def list_posts(sort_by: str = "created_at"):
    return db.execute(f"SELECT * FROM posts ORDER BY {sort_by}")   # injection!

✅ Correct — use Literal to whitelist allowed values:

def list_posts(sort_by: Literal["created_at", "view_count", "title"] = "created_at"):
    # sort_by is guaranteed to be one of the three allowed values ✓

Quick Reference

Pattern Code
Optional with default page: int = 1
Optional with None author_id: int | None = None
Required query param search: str (no default)
With constraints limit: Annotated[int, Query(ge=1, le=100)] = 10
List values tags: Annotated[list[str], Query()] = []
Enum-like order: Literal["asc", "desc"] = "desc"
Date param from_date: date | None = None

🧠 Test Yourself

A client sends GET /posts?tags=python&tags=fastapi&tags=api. Your handler has tags: Annotated[list[str], Query()] = []. What value does tags have inside the handler?