Capítulo 8, Básico
CRUD completo: uma lista de tarefas
Criar, ler, atualizar e remover é o esqueleto de quase toda API. Aqui eu monto um CRUD completo guardado em memória, e acerto os pontos em que o exemplo clássico dá respostas erradas.
O desenho
Um CRUD precisa de quatro rotas sobre o mesmo recurso, e eu as organizo em torno de três modelos: o que o cliente envia para criar (sem id), o que a API devolve (com id), e o que o cliente envia para uma alteração parcial (todos os campos opcionais). O id é gerado pelo servidor: deixar o cliente escolher o próprio id abre espaço para colisões.
from itertools import count
from fastapi import FastAPI, HTTPException, status
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class TarefaEntrada(BaseModel):
titulo: str = Field(min_length=1, max_length=100)
concluida: bool = False
class Tarefa(TarefaEntrada):
id: int
class TarefaParcial(BaseModel):
titulo: str | None = Field(default=None, min_length=1, max_length=100)
concluida: bool | None = None
tarefas: dict[int, Tarefa] = {}
proximo_id = count(1)
Uma lista funcionaria, mas um dicionário indexado pelo id encontra uma tarefa direto, sem percorrer tudo. O banco de dados real (capítulo 16) vai ocupar o lugar dele.
Criar e ler
O POST devolve 201 (criado), e o GET aceita um filtro opcional. A busca por id fica em uma função só, que levanta o 404 quando não acha:
def buscar_ou_404(tarefa_id: int) -> Tarefa:
tarefa = tarefas.get(tarefa_id)
if tarefa is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Tarefa não encontrada")
return tarefa
@app.post("/tarefas", response_model=Tarefa, status_code=status.HTTP_201_CREATED)
def criar(entrada: TarefaEntrada):
tarefa = Tarefa(id=next(proximo_id), **entrada.model_dump())
tarefas[tarefa.id] = tarefa
return tarefa
@app.get("/tarefas", response_model=list[Tarefa])
def listar(concluida: bool | None = None):
itens = list(tarefas.values())
if concluida is not None:
itens = [t for t in itens if t.concluida == concluida]
return itens
@app.get("/tarefas/{tarefa_id}", response_model=Tarefa)
def obter(tarefa_id: int):
return buscar_ou_404(tarefa_id)
Atualizar: PUT substitui, PATCH altera
O PUT substitui o recurso inteiro: o cliente manda todos os campos. O PATCH altera só o que veio, e o model_dump(exclude_unset=True) do capítulo anterior é o que torna isso possível, porque separa "o cliente mandou false" de "o cliente não mandou nada":
@app.put("/tarefas/{tarefa_id}", response_model=Tarefa)
def substituir(tarefa_id: int, entrada: TarefaEntrada):
buscar_ou_404(tarefa_id)
tarefas[tarefa_id] = Tarefa(id=tarefa_id, **entrada.model_dump())
return tarefas[tarefa_id]
@app.patch("/tarefas/{tarefa_id}", response_model=Tarefa)
def alterar(tarefa_id: int, mudancas: TarefaParcial):
atual = buscar_ou_404(tarefa_id)
tarefas[tarefa_id] = atual.model_copy(update=mudancas.model_dump(exclude_unset=True))
return tarefas[tarefa_id]
@app.delete("/tarefas/{tarefa_id}", status_code=status.HTTP_204_NO_CONTENT)
def remover(tarefa_id: int):
buscar_ou_404(tarefa_id)
del tarefas[tarefa_id]
O DELETE devolve 204 sem corpo: deu certo, e não há nada a mostrar.
O fluxo completo
cliente = TestClient(app)
a = cliente.post("/tarefas", json={"titulo": "Estudar FastAPI"})
b = cliente.post("/tarefas", json={"titulo": "Escrever testes", "concluida": True})
print(a.status_code, a.json())
print(b.json())
print([t["titulo"] for t in cliente.get("/tarefas").json()])
print([t["titulo"] for t in cliente.get("/tarefas?concluida=true").json()])
201 {'titulo': 'Estudar FastAPI', 'concluida': False, 'id': 1}
{'titulo': 'Escrever testes', 'concluida': True, 'id': 2}
['Estudar FastAPI', 'Escrever testes']
['Escrever testes']
print(cliente.put("/tarefas/1", json={"titulo": "Estudar FastAPI com calma"}).json())
print(cliente.patch("/tarefas/1", json={"concluida": True}).json())
print(cliente.delete("/tarefas/2").status_code)
print(cliente.get("/tarefas/2").status_code, cliente.get("/tarefas/2").json())
print(len(cliente.get("/tarefas").json()))
{'titulo': 'Estudar FastAPI com calma', 'concluida': False, 'id': 1}
{'titulo': 'Estudar FastAPI com calma', 'concluida': True, 'id': 1}
204
404 {'detail': 'Tarefa não encontrada'}
1
Repare no PATCH: ele mudou só concluida, e o título que o PUT tinha acabado de definir ficou intacto.
O que eu mudei em relação ao exemplo clássico
| Exemplo clássico | Aqui | Por quê |
|---|---|---|
O cliente envia o id | O servidor gera | O cliente pode repetir um id existente |
return {"error": "não encontrado"} | raise HTTPException(404) | O primeiro devolve 200 com uma mensagem de erro, e o cliente não percebe que falhou |
POST devolve 200 | 201 Created | O status diz o que aconteceu |
DELETE devolve uma mensagem | 204 No Content | Não há corpo a devolver |
Uma lista percorrida com for | Dicionário por id | A busca é direta |
| Um modelo para tudo | Entrada, saída e parcial | Cada um tem campos diferentes |
Memória não é banco de dados
Estes dados somem quando o servidor reinicia, e cada processo tem a sua cópia: com dois processos (
--workers 2), uma tarefa criada em um pode não existir no outro. Use isso só para aprender. O capítulo 16 troca o dicionário por um banco de verdade, sem mudar as rotas.
Exercício 1
Estatísticas das tarefas
Acrescente GET /estatisticas, que devolva {"total": ..., "concluidas": ..., "pendentes": ...}. Teste com tarefas novas, em um app limpo.