Search Topics

Search across all FastAPI topics

GitHub

Query Parameters

Core Concept

Query parameters are the key-value pairs after the ? in a URL. FastAPI handles them automatically through function parameters that are not part of the path.

You add limit: int as a query parameter. A user forgets to include it: GET /items. Boom — 422. Your endpoint is broken for every user who doesn't know about mandatory query params.

terminal
GET /items → 422 Unprocessable Entity

{
  "detail": [
    {
      "type": "missing",
      "loc": ["query", "limit"],
      "msg": "Field required",
      "input": null
    }
  ]
}

Question

Why should a simple list endpoint require the caller to know about a limit parameter? The answer: it shouldn't. The fix is a single = 10 after the parameter declaration. That tiny default value is the difference between a friendly API and a frustrating one.

How FastAPI Decides: Required vs. Optional

FastAPI looks at one thing to decide if a query parameter is required: does it have a default value?

Parameter declared

limit: int

Has default?

No default value

REQUIRED

422 if missing

Parameter declared

limit: int = 10

Has default?

Default is 10

OPTIONAL

Uses 10 if missing

Required vs optional

What you just learned

No default value = required query parameter (422 if missing)

With a default value = optional (uses the default if not provided)

This one rule prevents most query parameter 422 errors

Basic Query Parameters

Any function parameter that's not in the path string becomes a query parameter automatically. You don't need to declare it anywhere special — FastAPI just knows.

main.py
from fastapi import FastAPI

app = FastAPI()

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

# GET /items              → {"skip": 0, "limit": 10}  (defaults)
# GET /items?skip=20      → {"skip": 20, "limit": 10}
# GET /items?skip=20&limit=50 → {"skip": 20, "limit": 50}

Both skip and limit have defaults, so calling GET /items with no params works perfectly.

Required vs Optional — In Practice

Here's how the three flavors of query parameters look in real code. The search query is required (no default), while category and language are optional.

main.py
@app.get("/search")
async def search(
    q: str,                         # Required — no default
    category: str = "all",          # Optional — has default
    lang: str | None = None,        # Optional — explicit None
):
    return {"q": q, "category": category, "lang": lang}

# GET /search?q=fastapi           → ✓ (category="all", lang=None)
# GET /search                     → 422 "q is required"
# GET /search?q=fastapi&lang=en   → ✓

Query parameter patterns

What you just learned

Non-path function parameters automatically become query params

Required params (no default) return 422 when missing

str = 'default' always gives you a string; str | None = None might give you None

Adding Validation with Query()

Defaults are great, but what if you need to say "the search query must be at least 3 characters and no more than 50"? That's where Query() comes in. Pair it with Annotated for clean, readable code.

main.py
from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()

@app.get("/items")
async def list_items(
    q: Annotated[
        str | None,
        Query(
            min_length=3,
            max_length=50,
            pattern="^[a-zA-Z0-9 ]+$",
            title="Search Query",
            description="Search for items by name",
            examples=["fastapi", "python web"],
        ),
    ] = None,
    skip: Annotated[int, Query(ge=0, description="Items to skip")] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 10,
):
    return {"q": q, "skip": skip, "limit": limit}

# GET /items?q=ab           → 422 (min_length=3 violated)
# GET /items?q=fastapi      → ✓
# GET /items?limit=200      → 422 (le=100 violated)

List Query Parameters

Want to accept multiple tags in a single request? Just type your parameter as a list. The client repeats the key in the URL, and FastAPI collects them all.

main.py
from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()

@app.get("/items")
async def filter_items(
    tag: Annotated[list[str], Query()] = [],
):
    return {"tags": tag}

# GET /items?tag=python&tag=fastapi&tag=async
# → {"tags": ["python", "fastapi", "async"]}

# With validation:
@app.get("/search")
async def search(
    category: Annotated[
        list[str],
        Query(min_length=1, description="Filter categories"),
    ] = ["general"],
):
    return {"categories": category}

Query validation

What you just learned

Query() adds min/max length, regex patterns, and numeric constraints

Annotated[type, Query(...)] is the modern, recommended syntax

list[str] collects repeated keys into a Python list

Try It: Query String Builder

Add, remove, and edit query parameters to see how the URL builds up and what FastAPI receives. Try repeating a key to see list detection.

Go Deeper: Alias & Deprecated Params

Sometimes your URL needs a param name that isn't a valid Python variable (like item-query). And sometimes you need to keep an old parameter working while nudging users to the new one.

main.py
from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()

@app.get("/items")
async def list_items(
    # URL uses "item-query" but Python uses "item_query"
    item_query: Annotated[
        str | None,
        Query(alias="item-query"),
    ] = None,
    # Marked deprecated — shows warning in docs
    old_filter: Annotated[
        str | None,
        Query(
            deprecated=True,
            description="Use 'q' instead. Will be removed in v2.",
        ),
    ] = None,
):
    return {"q": item_query}

# GET /items?item-query=phone → item_query = "phone"
# The deprecated param still works but shows a warning in /docs

Boolean Query Parameters

Insight

FastAPI is surprisingly flexible with booleans. "true", "1", "yes", "on" — they all become True. This means your users don't have to remember the exact format.

main.py
@app.get("/items")
async def list_items(
    short: bool = False,
    include_deleted: bool = False,
):
    return {"short": short, "include_deleted": include_deleted}

# All of these set short=True:
# GET /items?short=true
# GET /items?short=1
# GET /items?short=yes
# GET /items?short=on
# GET /items?short=True

Think about it...

What's the difference between limit: int = 10 and limit: int | None = None as query parameter declarations?

Hint: Think about what value your code receives when the parameter is missing from the URL.

Key Points

Auto Detection

Non-path function params become query params automatically

Required vs Optional

No default = required (422 if missing). Default = optional.

Annotated + Query()

Add min/max length, pattern, ge/le, title, description

List Parameters

list[str] collects repeated keys: ?tag=a&tag=b → ["a", "b"]

Alias

Map URL param names to valid Python variables

Deprecated

Mark params for removal — still works but warns in docs