Chapter 9, Basic
Combining path, query, and body
In a real API, the same route receives an identifier, an option, and a set of data. FastAPI separates the three by how you declare each parameter.
How FastAPI decides where each parameter comes from
The rule is simple and applies to the entire route:
| If the parameter... | It comes from... |
|---|---|
Appears in the path ({id}) | The path |
Is a simple type (int, str, bool) outside the path | The parameters (?x=1) |
| Is a Pydantic model | The body (JSON) |
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UsuarioEntrada(BaseModel):
nome: str
idade: int
usuarios: dict[int, UsuarioEntrada] = {1001: UsuarioEntrada(nome="Mohit", idade=23)}
@app.put("/usuarios/{usuario_id}")
def atualizar(usuario_id: int, usuario: UsuarioEntrada, notificar: bool = False):
if usuario_id not in usuarios:
raise HTTPException(status_code=404, detail="Usuário não encontrado")
usuarios[usuario_id] = usuario
return {"mensagem": "Usuário atualizado", "notificar": notificar, "dados": usuario}
cliente = TestClient(app)
resposta = cliente.put("/usuarios/1001?notificar=true", json={"nome": "Mohit", "idade": 24})
print(resposta.status_code, resposta.json())
print(cliente.put("/usuarios/9", json={"nome": "X", "idade": 1}).json())
200 {'mensagem': 'Usuário atualizado', 'notificar': True, 'dados': {'nome': 'Mohit', 'idade': 24}}
{'detail': 'Usuário não encontrado'}
The three arrived at the same time: usuario_id from the path, notificar from the query and usuario of the body. You can check how FastAPI classified each one in the OpenAPI document:
operacao = app.openapi()["paths"]["/usuarios/{usuario_id}"]["put"]
print([(p["name"], p["in"]) for p in operacao["parameters"]])
print("requestBody" in operacao)
[('usuario_id', 'path'), ('notificar', 'query')]
True
Each part has a role: the path identifies which resource, the parameters adjusts how the operation happens, and the body carries what will be recorded.
Multiple models in the body
If the route declares two models, FastAPI expects a JSON with a key for each one, instead of a "loose" body:
class Item(BaseModel):
nome: str
class Comprador(BaseModel):
email: str
@app.post("/pedidos")
def criar_pedido(item: Item, comprador: Comprador):
return {"item": item.nome, "comprador": comprador.email}
corpo = {"item": {"nome": "Caneta"}, "comprador": {"email": "a@b.com"}}
print(cliente.post("/pedidos", json=corpo).json())
{'item': 'Caneta', 'comprador': 'a@b.com'}
A simple value inside the body
A simple type goes to the query, unless you mark it with Body. This is useful to add a loose field to the body, alongside a model:
from typing import Annotated
from fastapi import Body
@app.post("/avaliacoes")
def avaliar(item: Item, nota: Annotated[int, Body(ge=1, le=5)]):
return {"item": item.nome, "nota": nota}
print(cliente.post("/avaliacoes", json={"item": {"nome": "Caneta"}, "nota": 5}).json())
{'item': 'Caneta', 'nota': 5}
A body with a single key
With a single model, the body is the model itself. If you want it to come wrapped in a key (to maintain the format when the body grows later), use embed=True:
@app.post("/itens")
def criar_item(item: Annotated[Item, Body(embed=True)]):
return item
print(cliente.post("/itens", json={"item": {"nome": "Régua"}}).json())
{'nome': 'Régua'}
Who identifies, who filters, who carries
Before deciding where a data goes, I ask myself what its role is. If it identifies the resource, it's a path. If it adjusts an operation or filters a list, it's a query. If it's the content that will be created or altered, it's the body.
Exercise 1
Update a product with discount
Create PUT /produtos/{produto_id} with body ProdutoEntrada (nome, preco) and the query parameter aplicar_desconto (default False). If true, save the price with a 10% discount. Return 404 for a non-existent id.