Capítulo 53, Backend
REST e fundamentos de APIs
Uma API é um contrato entre quem a oferece e quem a consome. Quando o contrato é previsível, os clientes funcionam sem você precisar explicar cada rota.
Código deste capítulo: backend/cap53_rest_contratos.py
Recursos, não ações
Em REST você modela coisas (recursos) com URLs no plural, e o verbo HTTP diz o que fazer com elas. POST /pedidos cria. GET /pedidos/7 lê. POST /criarPedido é o erro clássico: o verbo já está no método.
| Verbo | Faz | Seguro | Idempotente |
|---|---|---|---|
GET | Lê | Sim | Sim |
POST | Cria (ou executa uma ação) | Não | Não |
PUT | Substitui por inteiro | Não | Sim |
PATCH | Altera parcialmente | Não | Depende |
DELETE | Remove | Não | Sim |
Seguro significa que não altera estado. Idempotente significa que repetir a chamada dá o mesmo resultado que chamar uma vez. O POST é o único que não é idempotente, e por isso é o que mais merece atenção.
Códigos de status que importam
| Código | Quando usar |
|---|---|
200 | Sucesso com corpo |
201 | Recurso criado (devolva o recurso) |
204 | Sucesso sem corpo (um DELETE, por exemplo) |
400 | Pedido malformado |
401 | Sem credencial, ou credencial inválida |
403 | Credencial válida, mas sem permissão |
404 | O recurso não existe |
409 | Conflito com o estado atual (cancelar um pedido já cancelado) |
422 | O formato está certo, mas os dados não passam na validação |
429 | Limite de requisições excedido |
503 | Serviço indisponível |
Paginação: por deslocamento ou por cursor
A paginação por deslocamento (?pagina=2) é a mais intuitiva e tem um defeito sério: se alguém cria um item enquanto você navega, os itens se deslocam e você vê um repetido (ou perde um). A paginação por cursor pergunta "o que vem depois do último que eu vi?", e por isso é estável:
itens = list(range(1, 11))
def por_deslocamento(itens, deslocamento, limite):
return itens[deslocamento : deslocamento + limite]
pagina_1 = por_deslocamento(itens, 0, 3)
itens.insert(0, 0) # alguém cria um item no começo enquanto você navega
pagina_2 = por_deslocamento(itens, 3, 3)
print(pagina_1, pagina_2)
[1, 2, 3] [3, 4, 5]
O 3 apareceu duas vezes. Com cursor:
itens = list(range(1, 11))
def por_cursor(itens, depois_de, limite):
pagina = [i for i in itens if i > depois_de][:limite]
proximo = pagina[-1] if len(pagina) == limite else None
return pagina, proximo
pagina_1, cursor = por_cursor(itens, 0, 3)
itens.insert(0, 0)
pagina_2, cursor = por_cursor(itens, cursor, 3)
print(pagina_1, pagina_2)
[1, 2, 3] [4, 5, 6]
Em um banco, o cursor vira WHERE id > :cursor ORDER BY id LIMIT :limite, que usa o índice da chave primária e continua rápido na página um milhão. O deslocamento (OFFSET) precisa varrer e descartar todas as linhas anteriores.
Erros com um formato único
Um cliente precisa tratar erro de forma programática. Isso exige que todos os erros da API tenham o mesmo formato, e não texto livre que muda por rota. O padrão é o problem details (RFC 9457): um objeto com tipo, título, status e detalhe, e a mesma resposta serve para pessoas e para código:
def problema(status, tipo, titulo, detalhe, **extras):
return {
"tipo": f"https://exemplo.com/erros/{tipo}",
"titulo": titulo,
"status": status,
"detalhe": detalhe,
**extras,
}
print(problema(409, "pedido-ja-cancelado", "Conflito de estado", "O pedido 7 já está cancelado.", pedido_id=7))
{'tipo': 'https://exemplo.com/erros/pedido-ja-cancelado', 'titulo': 'Conflito de estado', 'status': 409, 'detalhe': 'O pedido 7 já está cancelado.', 'pedido_id': 7}
O Content-Type dessa resposta é application/problem+json. O cliente decide pelo tipo (estável), e o detalhe é só para leitura humana.
Idempotência no POST
O POST não é idempotente, mas você pode torná-lo. O cliente gera uma chave única (Idempotency-Key) por operação e a reenvia nas retentativas. O servidor guarda o resultado da primeira vez e devolve o mesmo nas seguintes. Assim um timeout de rede não gera dois pedidos:
resultados = {}
def criar_pedido(chave, dados):
if chave in resultados:
return 200, resultados[chave]
pedido = {"id": len(resultados) + 1, **dados}
resultados[chave] = pedido
return 201, pedido
print(criar_pedido("k1", {"cliente": "Ana"}))
print(criar_pedido("k1", {"cliente": "Ana"}))
print(criar_pedido("k2", {"cliente": "Bia"}))
(201, {'id': 1, 'cliente': 'Ana'})
(200, {'id': 1, 'cliente': 'Ana'})
(201, {'id': 2, 'cliente': 'Bia'})
Repare nos códigos: 201 na criação e 200 quando a chave já existia. Em produção, a verificação "essa chave já existe?" não basta (duas requisições simultâneas passam juntas por ela), e a garantia vem de uma restrição UNIQUE no banco. O capítulo da API completa mostra isso.
Versionar e evoluir
Uma API publicada é difícil de mudar, porque você não controla os clientes. A regra é: acrescentar campos e rotas é compatível. Remover ou renomear um campo, mudar um tipo ou apertar uma validação quebra quem já usa. Para uma mudança incompatível, publique uma nova versão (/v2/pedidos) e mantenha a antiga por um prazo combinado.
O contrato como código
Eu escrevo o contrato primeiro, em forma de schemas. O FastAPI (próximo capítulo) gera a documentação OpenAPI a partir deles automaticamente, e a mesma definição serve para validar a entrada, serializar a saída e documentar. Documentação escrita à mão envelhece, e a gerada do código não.
Exercício 1
Paginação por cursor
Escreva paginar(itens, cursor, limite) que devolva {"itens": [...], "proximo_cursor": ...}. Busque um item a mais que o limite para saber se existe próxima página, e use None quando não houver.