Query Parameters

beginner fastapi query-params validation

Query parameters are the ?key=value&other=thing bits after the URL path. In FastAPI we read them by declaring function arguments that aren’t in the path string. That’s it — no request.query.get() ceremony.

The basic rule

If we declare a function param whose name doesn’t appear in the route path, FastAPI treats it as a query param.

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def list_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit, "items": [...]}

Hit /items/?skip=20&limit=50 and FastAPI parses both into ints. Defaults make them optional — call /items/ and we get skip=0, limit=10.

Required vs optional

A query param is optional if it has a default value. It’s required if it doesn’t.

@app.get("/search/")
async def search(q: str):            # required
    return {"q": q}

@app.get("/search/v2/")
async def search_v2(q: str = ""):    # optional, defaults to empty
    return {"q": q}

@app.get("/search/v3/")
async def search_v3(q: str | None = None):  # optional, may be None
    return {"q": q}

str | None = None (or Optional[str] = None) is the pattern when “missing” is genuinely different from “empty string.”

Default value decides required-ness
q: str  →  required (no default)
q: str = "foo"  →  optional with default
q: str | None = None  →  optional, nullable

Type coercion

Booleans are forgiving:

@app.get("/items/")
async def list_items(in_stock: bool = False):
    return {"in_stock": in_stock}

?in_stock=true, ?in_stock=1, ?in_stock=yes, ?in_stock=on all parse to True. Their false counterparts go to False. Anything else gets a 422.

Validation with Query()

For length, regex, or numeric constraints, use the Query() helper:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/products/")
async def search_products(
    q: str = Query(
        ...,                       # ... means required
        min_length=3,
        max_length=50,
        title="Search query",
        description="Free-text search across product names",
    ),
    page: int = Query(1, ge=1, le=1000),
    sort: str = Query("created", pattern="^(created|price|name)$"),
):
    return {"q": q, "page": page, "sort": sort}
  • min_length, max_length — string length bounds.
  • pattern — regex match.
  • ge, gt, le, lt — numeric bounds.
  • title, description — show up in /docs.

List query parameters

Some APIs need to accept multiple values for the same key: ?tag=python&tag=fastapi. We type the param as a list:

@app.get("/articles/")
async def list_articles(tag: list[str] = Query(default=[])):
    return {"tags": tag}

A request like /articles/?tag=python&tag=fastapi&tag=backend gives us tag = ["python", "fastapi", "backend"].

Without Query(default=[]), FastAPI would think list[str] is a request body, not a query param. The explicit Query() is what tells it “this is a query param that can repeat.”

Alias for keys with weird characters

OpenAPI / URL conventions often use kebab-case but Python uses snake_case. The alias keeps both happy:

@app.get("/items/")
async def list_items(item_query: str | None = Query(default=None, alias="item-query")):
    return {"item_query": item_query}

The URL sees ?item-query=hello, the function gets item_query="hello".

Deprecation

Mark a query param deprecated and Swagger shows it crossed out:

@app.get("/items/")
async def list_items(q: str | None = Query(default=None, deprecated=True)):
    return {"q": q}

Combining path + query

Path params and query params coexist naturally:

@app.get("/users/{user_id}/orders/")
async def user_orders(
    user_id: int,
    status: str | None = None,
    limit: int = 20,
):
    return {"user_id": user_id, "status": status, "limit": limit}

user_id is in the path string so it’s a path param. status and limit aren’t, so they’re query params. FastAPI figures it out from the route declaration alone.

Interview tip

The “is this a path param or query param?” question comes up a lot. The rule: if the parameter name appears in the URL pattern between {}, it’s a path param. Otherwise (assuming it’s a simple type, not a Pydantic model) it’s a query param.