Capítulo 9, Básico
Combinando caminho, consulta e corpo
Em uma API real, a mesma rota recebe um identificador, uma opção e um conjunto de dados. O FastAPI separa os três pela forma como você declara cada parâmetro.
Como o FastAPI decide de onde vem cada parâmetro
A regra é simples e vale para a rota inteira:
| Se o parâmetro... | Ele vem de... |
|---|---|
Aparece no caminho ({id}) | O caminho |
É um tipo simples (int, str, bool) fora do caminho | A consulta (?x=1) |
| É um modelo do Pydantic | O corpo (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'}
Os três chegaram ao mesmo tempo: usuario_id do caminho, notificar da consulta e usuario do corpo. Dá para conferir como o FastAPI classificou cada um no documento OpenAPI:
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
Cada parte tem um papel: o caminho identifica qual recurso, a consulta ajusta como a operação acontece, e o corpo carrega o quê será gravado.
Vários modelos no corpo
Se a rota declara dois modelos, o FastAPI espera um JSON com uma chave para cada um, em vez de um corpo "solto":
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'}
Um valor simples dentro do corpo
Um tipo simples vai para a consulta, a não ser que você o marque com Body. Isso é útil para acrescentar um campo avulso ao corpo, ao lado de um modelo:
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}
Um corpo com uma única chave
Com um modelo só, o corpo é o próprio modelo. Se você quiser que ele venha embrulhado em uma chave (para manter o formato quando o corpo cresce depois), 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'}
Quem identifica, quem filtra, quem carrega
Antes de decidir onde um dado vai, eu me pergunto qual é o papel dele. Se identifica o recurso, é caminho. Se ajusta uma operação ou filtra uma lista, é consulta. Se é o conteúdo que será criado ou alterado, é corpo.
Exercício 1
Atualizar um produto com desconto
Crie PUT /produtos/{produto_id} com corpo ProdutoEntrada (nome, preco) e o parâmetro de consulta aplicar_desconto (padrão False). Se for verdadeiro, grave o preço com 10% de desconto. Devolva 404 para um id inexistente.