DevGuideTK
← Trilha Backend

Design de APIs REST

Recursos, nomes, códigos de status e formato de erros em uma API previsível.

Os exemplos implementam a mesma rota nas cinco linguagens: POST /pedidos, que valida o corpo, cria o pedido e responde 201 Created com o cabeçalho Location, ou devolve o erro no formato padrão de Problem Details (RFC 9457).

Criar um recurso, com erros no formato padrão

import http from "node:http";
import { randomUUID } from "node:crypto";

type Item = { produtoId: string; quantidade: number };
type Pedido = { id: string; clienteId: string; itens: Item[]; status: string; criadoEm: string };

const pedidos = new Map<string, Pedido>();
const ERROS = "https://api.loja.com.br/erros";

function problema(res: http.ServerResponse, status: number, tipo: string, titulo: string, extra = {}) {
  res.writeHead(status, { "Content-Type": "application/problem+json" });
  res.end(JSON.stringify({ type: `${ERROS}/${tipo}`, title: titulo, status, ...extra }));
}

function validar(corpo: any): Record<string, string> {
  const erros: Record<string, string> = {};
  if (typeof corpo?.clienteId !== "string" || !corpo.clienteId) erros.clienteId = "obrigatório";
  if (!Array.isArray(corpo?.itens) || corpo.itens.length === 0) erros.itens = "precisa de pelo menos um item";
  corpo?.itens?.forEach?.((item: any, i: number) => {
    if (!Number.isInteger(item?.quantidade) || item.quantidade < 1) erros[`itens[${i}].quantidade`] = "inteiro maior que zero";
  });
  return erros;
}

http.createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/pedidos") {
    return problema(res, 404, "nao-encontrado", "Recurso não encontrado");
  }

  let corpo: any;
  try {
    let texto = "";
    for await (const parte of req) texto += parte;
    corpo = JSON.parse(texto);
  } catch {
    return problema(res, 400, "json-invalido", "O corpo não é um JSON válido");
  }

  const erros = validar(corpo);
  if (Object.keys(erros).length > 0) {
    return problema(res, 422, "dados-invalidos", "Os dados enviados são inválidos", { errors: erros });
  }

  const pedido: Pedido = {
    id: randomUUID(),
    clienteId: corpo.clienteId,
    itens: corpo.itens,
    status: "aberto",
    criadoEm: new Date().toISOString(),
  };
  pedidos.set(pedido.id, pedido);

  res.writeHead(201, { "Content-Type": "application/json", Location: `/pedidos/${pedido.id}` });
  res.end(JSON.stringify(pedido));
}).listen(3000);

O resultado, nas quatro versões executadas

Rodamos as versões em TypeScript, Python, Go e C# e mandamos três pedidos a cada uma: um válido, um com dados inválidos (cliente vazio e quantidade zero) e um com JSON malformado.

PedidoTypeScriptPython (FastAPI)GoC# (ASP.NET)
Válido201, Location201, Location201, Location201, Location
Dados inválidos422, problem+json422, problem+json422, problem+json422, problem+json
JSON malformado400, problem+json400, problem+json400, problem+json400, problem+json

O formato padrão é o mesmo nas quatro; os detalhes, não:

Mensagens de validaçãoerrorstypeExtras
TypeScriptAs do seu códigoTexto por campoO seu catálogo de errosNenhum
PythonAs do Pydantic, em inglês (String should have at least 1 character)Texto por campo, com o caminho itens.0.quantidadeO seu catálogoNenhum
GoAs do seu códigoTexto por campoO seu catálogoCampos em ordem alfabética
JavaAs do Bean Validation, no idioma da configuraçãoNão incluídos por padrão: acrescente num @RestControllerAdvicePadrão do Spring, personalizávelinstance com o caminho
C#As do seu códigoLista de textos por campoLink para a RFC (rfc4918#section-11.2), se não for definidotraceId

Num mesmo sistema, escolha uma forma e mantenha nos serviços todos: um cliente que integra com três serviços não deveria tratar três formatos de erro. Se as mensagens vão aparecer para o usuário final, traduza as do framework ou use códigos de erro que o cliente traduz.

A versão em Java não foi executada: ela depende de um projeto Spring Boot completo. Com spring.mvc.problemdetails.enabled=true, o Spring Boot 3 responde aos erros de validação com application/problem+json, mas com status 400, e não 422. Para usar 422, trate a MethodArgumentNotValidException num @RestControllerAdvice.

Ler um recurso, com o código certo para cada caso

GET /pedidos/981 HTTP/1.1
SituaçãoResposta
O pedido existe e pertence ao usuário200 com o pedido
O pedido não existe404 com problem+json
O pedido existe e é de outro usuário404, e não 403: não confirme a existência de recursos alheios
Sem credencial401
O identificador tem formato inválido400 ou 404; escolha um e mantenha

A terceira linha é uma decisão de segurança. Responder 403 para pedidos de outros usuários e 404 para os que não existem permite descobrir quais identificadores são válidos.

Testar a API pela linha de comando

# Criar: espera 201 e o cabeçalho Location
curl -i -X POST http://localhost:3000/pedidos \
  -H 'Content-Type: application/json' \
  -d '{"clienteId":"42","itens":[{"produtoId":"7","quantidade":2}]}'

# Dados inválidos: espera 422 e application/problem+json
curl -i -X POST http://localhost:3000/pedidos \
  -H 'Content-Type: application/json' \
  -d '{"clienteId":"","itens":[{"produtoId":"7","quantidade":0}]}'

# JSON malformado: espera 400
curl -i -X POST http://localhost:3000/pedidos \
  -H 'Content-Type: application/json' -d '{"clienteId":'

Esses três pedidos são a base de um teste automatizado de contrato: o status, o Content-Type e os campos do erro fazem parte da API tanto quanto os campos de sucesso.

Lista de revisão

Recursos e URLs

  • Os caminhos usam substantivos no plural (/pedidos, /pedidos/981/itens), sem verbos?
  • Ações que não são criar, ler, alterar ou remover viram sub-recursos (POST /pedidos/981/cancelamento)?
  • Os nomes de campos seguem um único padrão em toda a API?

Respostas

  • Criar devolve 201 com Location; remover devolve 204?
  • Erros do cliente são 4xx, falhas do servidor 5xx, e nenhum erro sai com 200?
  • Todos os erros seguem o mesmo formato, de preferência application/problem+json?
  • Datas em ISO 8601 com fuso, valores monetários sem ponto flutuante?

Segurança e contrato

  • Recursos de outros usuários respondem 404, e não 403?
  • A resposta não expõe campos internos (senhas, chaves, colunas que só o banco usa)?
  • Existe teste automatizado para os casos de erro, e não só para o de sucesso?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar uma rota de criação do jeito certo

    Quero implementar uma rota de criação de recurso seguindo as boas práticas de API REST.
    
    Pergunte qual linguagem e framework eu uso.
    
    Vamos em etapas. Em cada uma, deixe que eu tente e depois mostre a sua versão:
    1. A rota POST que cria um pedido e responde 201 com o cabeçalho Location.
    2. A validação dos dados, com erro 422 listando cada campo inválido.
    3. O tratamento de JSON malformado com 400.
    4. Todos os erros no formato application/problem+json, inclusive os gerados pelo próprio framework.
    5. A rota GET que devolve 404 para pedidos inexistentes e para pedidos de outros usuários.
    6. Testes automatizados para os casos de sucesso e de erro, conferindo status, Content-Type e corpo.
    
    No final, me mostre como testar tudo com curl.
  • Padronizar os erros do meu framework

    Quero que todos os erros da minha API sigam o formato Problem Details, inclusive os gerados pelo framework.
    
    Pergunte qual linguagem e framework eu uso.
    
    Explique, com código:
    1. Como o meu framework formata os erros por padrão: validação, rota inexistente, método não permitido, exceção não tratada.
    2. Como trocar cada um pelo formato application/problem+json.
    3. Como definir um catálogo de tipos de erro com endereços estáveis.
    4. Como traduzir as mensagens de validação do framework.
    5. Como garantir que uma exceção não tratada não vaze detalhes internos na resposta.
    
    Termine com um teste que confere o formato de cada tipo de erro.
  • Revisar o código de uma rota

    Vou colar abaixo o código de uma ou mais rotas da minha API. Revise com foco no desenho e no contrato.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando a linha:
    1. O método e o endereço combinam com a ação?
    2. O status de sucesso está correto, com Location ao criar?
    3. A validação devolve 422 com os campos inválidos, e JSON malformado devolve 400?
    4. Os erros seguem o formato padrão da API?
    5. A resposta usa um formato próprio, e não a entidade do banco?
    6. Recursos de outros usuários respondem 404?
    7. Há testes para os casos de erro?
    
    Para cada problema, explique a consequência para quem consome a API e mostre a correção.
    
    Código:
    [cole aqui]