The query string is where clients send filter, pagination, and search parameters — and also where invalid parameters slip through most easily: skip=-5, limit=100000, a search keyword of just one character. FastAPI ships with Query() to declare limits for each parameter: FastAPI validates them before your code runs and returns a 422 error when a parameter is invalid. This post uses Query() to lock down the query string in 5 minutes.

The Problem

A bare query parameter declaration only makes FastAPI check the type — the value itself is left unchecked. A negative skip, a huge limit, or a too-short keyword all slip through, and the problem only surfaces when the database slows to a crawl or the results come back wrong:

from fastapi import FastAPI
app = FastAPI()
@app.get("/products")
def list_products(skip: int = 0, limit: int = 10, q: str = ""):
# skip=-5, limit=100000, q="a" đều lọt qua
return {"skip": skip, "limit": limit, "q": q}

With that endpoint, GET /products?skip=-5&limit=100000 still returns 200 OK: a negative skip throws the offset off, an oversized limit pulls the whole table into memory, and a one-character q makes the search almost meaningless while the server still has to run the query. To block them you would have to write an if for each parameter — the code gets longer and it is easy to miss one.

The Solution

Declare the limits right inside Annotated with Query() — FastAPI validates them before the endpoint runs, and an invalid parameter returns 422 automatically:

from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/products")
def list_products(
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
q: Annotated[str | None, Query(min_length=2, max_length=50)] = None,
sort: Annotated[str, Query(pattern="^(asc|desc)$")] = "asc",
category: Annotated[list[str] | None, Query()] = None,
):
return {
"skip": skip,
"limit": limit,
"q": q,
"sort": sort,
"category": category,
}

Those four constraints cover every common query string pattern: ge/le bound the numeric range for pagination, min_length/max_length bound string length, pattern allows only asc or desc, and list[str] accepts repeated values. Verified with TestClient: skip=-1, limit=500, q=a, and sort=up all return 422 with a message naming exactly which parameter is wrong; ?category=book&category=toy returns 200 with ["book", "toy"].

Note: Query() applies only to query parameters — do not confuse it with Path() for path parameters or Field() for the request body.

Putting It Into Practice

Open the list or search endpoint your users hit most in your project and check three things: does pagination have an upper limit, does the search keyword have a minimum length, and is the sort parameter locked to just a few allowed values. Add Query() at exactly those three spots and you are done — no changes to the logic inside, no extra if statements, and the API is both safer and returns a standard error to the client right at the gateway.

Related Posts

FAQ

How is Query() different from a bare parameter declaration?

A bare parameter only checks the type: skip: int = 0 accepts both -5 and 100000. Query(ge=0) adds a value constraint — FastAPI rejects invalid parameters and returns 422 with a message naming exactly which parameter is wrong and why.

When should I use Query(), and when should I use Path()?

Use Query() for parameters after the ?, like ?skip=0&limit=20 — filter, pagination, and search parameters, which are usually optional and have default values. Use Path() for parameters inside the path, like /products/{product_id} — resource identifiers, which are required and have no default value.

How do I accept multiple values for the same query parameter?

Declare the type as list[str], like category in the example above, and the client repeats the parameter: ?category=book&category=toy. FastAPI collects them into the list ["book", "toy"].

Can Query() validate complex types like dates or Enums?

Yes. Declare the type as datetime.date and FastAPI parses ISO strings like 2026-11-06 itself, returning 422 for a wrong format. For a fixed set of values, declare an Enum instead of a pattern — the code reads more clearly and the OpenAPI docs auto-generate the list of allowed values.

💬 Have you tried this approach in your own project? If you ran into an error or know a more concise way to do it, leave a comment below — I read every one and will add the best ideas to the post.

Sources