Chapter 5, Basic
Query parameters
What comes after the `?` in the URL: filters, search, sorting, pagination. They adjust **how** you want to see the data, and not **which** data is the resource.
Required and optional parameters
Any function parameter that does not form part of the path becomes a query parameter. Without a default value, it is required. With a default value, it is optional:
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/busca")
def buscar(q: str, limite: int = 10, ordem: str | None = None):
return {"q": q, "limite": limite, "ordem": ordem}
cliente = TestClient(app)
print(cliente.get("/busca?q=caneta").json())
print(cliente.get("/busca?q=caneta&limite=50&ordem=preco").json())
{'q': 'caneta', 'limite': 10, 'ordem': None}
{'q': 'caneta', 'limite': 50, 'ordem': 'preco'}
The limite assumed the default 10 when none was provided. The ordem stayed None (JSON null). The q has no standard, and forgetting it is a mistake:
resposta = cliente.get("/busca")
erro = resposta.json()["detail"][0]
print(resposta.status_code, erro["loc"], erro["type"])
422 ['query', 'q'] missing
Optional: `str | None = None`, not `str = None`
A lot of material writes nome: str = None. It works, but ly about the type: the function says it receives a str and returns None by default. The type checker complains, and the OpenAPI document loses the information that the value can be null:
@app.get("/a")
def com_none_simples(nome: str = None):
return nome
@app.get("/b")
def com_uniao(nome: str | None = None):
return nome
caminhos = app.openapi()["paths"]
print(caminhos["/a"]["get"]["parameters"][0]["schema"])
print(caminhos["/b"]["get"]["parameters"][0]["schema"])
{'type': 'string', 'title': 'Nome'}
{'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Nome'}
The second form describes the truth (string or null), and it is the one I always use.
Booleans and lists
A bool accepts true, false, 1, 0, yes, no, on and off. To receive multiple values for the same parameter, declare a list with Query:
from typing import Annotated
from fastapi import Query
@app.get("/produtos")
def produtos(ativo: bool = True, etiquetas: Annotated[list[str] | None, Query()] = None):
return {"ativo": ativo, "etiquetas": etiquetas}
print(cliente.get("/produtos?ativo=no").json())
print(cliente.get("/produtos?etiquetas=novo&etiquetas=oferta").json())
{'ativo': False, 'etiquetas': None}
{'ativo': True, 'etiquetas': ['novo', 'oferta']}
Validation with `Query`
The Query imposes minimum and maximum size on texts and limits on numbers, and the rules go to the documentation:
@app.get("/pesquisa")
def pesquisa(
termo: Annotated[str, Query(min_length=3, max_length=20)],
pagina: Annotated[int, Query(ge=1)] = 1,
por_pagina: Annotated[int, Query(ge=1, le=100)] = 20,
):
return {"termo": termo, "pagina": pagina, "por_pagina": por_pagina}
print(cliente.get("/pesquisa?termo=caneta").json())
resposta = cliente.get("/pesquisa?termo=ab&por_pagina=500")
print(resposta.status_code, [e["loc"][-1] for e in resposta.json()["detail"]])
{'termo': 'caneta', 'pagina': 1, 'por_pagina': 20}
422 ['termo', 'por_pagina']
The two errors came back together: the termo too short and the por_pagina above 100. FastAPI validates everything and informs everything at once, instead of stopping at the first problem.
| Parameter | Goes in | Required? |
|---|---|---|
/usuarios/{id} | Path | Always |
?q=abc without default | Query | Yes |
?q=abc with default or None | Query | No |
| Pydantic Object | Body (next chapter) | According to the model |
Exercise 1
A search with limits
Create GET /livros with the parameters autor (optional), ano_minimo (integer, default 1900, minimum 1000) and limite (integer, default 10, between 1 and 50). Check the patterns and an invalid value.