Design de APIs REST: os princípios que separam uma API agradável de uma dor de cabeça
Recursos, verbos, códigos de status, versionamento e paginação: um guia prático para projetar APIs REST que os consumidores entendem e que envelhecem bem.
Neste artigo
Uma API é um contrato que você não pode quebrar sozinho#
Projetar uma API é diferente de escrever qualquer outro código, e a diferença está numa palavra: contrato. Uma função interna você reescreve à vontade; se mudar a assinatura, o compilador aponta todos os chamadores e você conserta. Uma API pública é o oposto — os chamadores estão em máquinas que você não controla, escritos por pessoas que você não conhece, e uma mudança incompatível quebra silenciosamente sistemas em produção mundo afora. O design de uma API é, antes de tudo, o design de um compromisso de longo prazo que você não pode desfazer unilateralmente.
Isso muda a natureza das decisões. Num código interno, uma escolha ruim custa uma refatoração. Numa API, uma escolha ruim custa anos de suporte a um formato que você lamenta, porque removê-lo quebraria consumidores. Por isso vale gastar tempo desproporcional no design inicial: a superfície de uma API é fácil de expandir e quase impossível de encolher. O estilo REST, apesar de mais de duas décadas de existência e de alternativas mais novas, continua sendo o vocabulário compartilhado da maioria das integrações — e usá-lo bem é uma questão de disciplina, não de sofisticação.
Recursos, não ações: o pilar do REST#
O erro conceitual mais comum ao projetar uma API REST é pensar em termos de ações — "buscar usuário", "criar pedido", "cancelar assinatura" — e traduzir cada uma numa URL própria: /buscarUsuario, /criarPedido, /cancelarAssinatura. Isso é RPC disfarçado de REST, e produz uma API sem coerência, onde cada endpoint é uma invenção nova que o consumidor precisa aprender do zero.
REST propõe uma inversão: você não modela ações, modela recursos — as coisas do seu domínio — e usa os verbos HTTP para expressar as ações sobre elas. Um usuário é um recurso, identificado por uma URL estável: /usuarios/42. O que você faz com ele vem do método HTTP:
GET /usuarios/42— lê o recurso, sem efeito colateral.POST /usuarios— cria um novo usuário na coleção.PUT /usuarios/42— substitui o recurso inteiro.PATCH /usuarios/42— atualiza parcialmente.DELETE /usuarios/42— remove o recurso.
A beleza dessa abordagem é a previsibilidade. Uma vez que o consumidor entende o padrão, ele consegue adivinhar como interagir com qualquer recurso novo, porque a gramática é sempre a mesma. Comparado a decorar dezenas de nomes de endpoints RPC arbitrários, é uma redução enorme de carga cognitiva. As URLs viram substantivos no plural (/pedidos, /produtos, /comentarios), a hierarquia expressa relacionamento (/pedidos/17/itens), e os verbos ficam por conta do HTTP, não da URL.
Uma propriedade importante que decorre disso é a distinção entre métodos seguros e idempotentes. GET é seguro: não altera nada, pode ser chamado à vontade. PUT e DELETE são idempotentes: chamá-los repetidas vezes produz o mesmo estado final que chamá-los uma vez — deletar um recurso já deletado ainda resulta em "não existe". POST não é idempotente: dois POST criam dois recursos. Respeitar essas semânticas não é purismo; é o que permite que clientes, proxies e caches ao longo do caminho tratem as requisições corretamente, inclusive fazendo retry seguro de operações idempotentes quando a rede falha.
Códigos de status: comunique o resultado na linguagem do HTTP#
O segundo pilar de uma API bem-comportada é usar os códigos de status HTTP com precisão. Eles são um vocabulário padronizado que qualquer cliente já entende — desde que você os use corretamente. O anti-padrão infelizmente comum é a API que retorna 200 OK para tudo, inclusive erros, colocando o resultado real num campo do corpo ({"sucesso": false, "erro": "..."}). Isso obriga cada cliente a ignorar o mecanismo padrão e inventar o próprio, e quebra caches e ferramentas que confiam no status.
O agrupamento dos códigos já carrega o significado essencial:
- 2xx — sucesso.
200 OKpara leituras e atualizações;201 Createdquando um recurso nasce (com o cabeçalhoLocationapontando para ele);204 No Contentquando a operação deu certo mas não há corpo a retornar. - 4xx — erro do cliente. A culpa é da requisição, e repeti-la igual não vai adiantar.
400 Bad Requestpara entrada malformada;401 Unauthorizedpara falta de autenticação;403 Forbiddenpara falta de permissão;404 Not Foundpara recurso inexistente;409 Conflictpara uma colisão de estado (criar algo que já existe);422para entrada bem-formada mas semanticamente inválida. - 5xx — erro do servidor. A culpa é sua, e o cliente não fez nada de errado.
500 Internal Server Errorpara falhas inesperadas;503 Service Unavailablequando o serviço está temporariamente fora.
A distinção 4xx versus 5xx é especialmente carregada de significado operacional: um cliente bem-feito faz retry de um 5xx (foi problema transitório do servidor) mas não de um 400 (a requisição está errada, repetir não muda nada). Confundir os dois — retornar 500 para uma entrada inválida — faz clientes tentarem de novo indefinidamente uma requisição que jamais vai passar, e enche seus logs de alarmes falsos. Aliás, retornar 500 para input hostil é também um sinal de fragilidade: uma API robusta rejeita entrada malformada com um 4xx claro, nunca deixa ela escapar até virar uma exceção não tratada de servidor.
O corpo do erro merece cuidado próprio. Um erro útil traz um código de erro estável (que o cliente pode tratar programaticamente), uma mensagem legível, e — quando aplicável — quais campos falharam a validação. E há uma regra de segurança inegociável: nunca vaze detalhes internos no corpo do erro. O stack trace, a query SQL que falhou, o nome da tabela, a versão do framework — tudo isso é informação que ajuda um atacante e não ajuda o cliente legítimo. Erros 5xx devem ser genéricos para o cliente e detalhados apenas nos seus logs internos, correlacionados por um identificador de rastreamento.
Versionamento e evolução sem quebrar ninguém#
Como toda API precisa evoluir e nenhuma pode quebrar consumidores existentes, o versionamento é parte inescapável do design. A pergunta não é se você vai versionar, mas como — e a resposta começa por entender o que é uma mudança compatível e o que é uma quebra.
Mudanças aditivas costumam ser seguras: adicionar um novo campo opcional na resposta, um novo endpoint, um novo parâmetro opcional. Um cliente bem-escrito ignora campos que não conhece, então acrescentar não quebra. Mudanças que removem ou alteram o contrato existente são quebras: renomear um campo, mudar seu tipo, tornar obrigatório um parâmetro que era opcional, alterar o significado de um valor. Essas exigem uma nova versão.
A prática mais direta é versionar na URL (/v1/pedidos, /v2/pedidos), o que torna a versão explícita e fácil de rotear. Outras equipes preferem versionar por cabeçalho, mantendo a URL do recurso estável. Qualquer que seja o mecanismo, o princípio operacional é o mesmo: quando você lança uma v2, mantém a v1 funcionando por um período de transição anunciado, com uma política clara de depreciação, para dar aos consumidores tempo de migrar. Cortar uma versão sem aviso é o pecado capital de uma API pública.
Um bom hábito que reduz a pressão por novas versões é ser conservador no que você promete e liberal no que aceita. Não exponha na resposta campos internos que você pode querer remover; não aceite formatos que você não pretende suportar para sempre. Cada elemento da superfície da API é um compromisso — quanto menor a superfície, menor a chance de precisar quebrá-la.
Idempotência e o mundo real das redes falhas#
Há um aspecto do design de APIs que só se revela quando você para de imaginar clientes bem-comportados numa rede perfeita e começa a lidar com a realidade: redes falham no meio das requisições. Um cliente envia um POST para criar um pedido, a requisição chega e é processada, mas a resposta se perde no caminho de volta. O cliente, sem receber confirmação, faz o que qualquer cliente robusto faz: tenta de novo. E agora você tem dois pedidos idênticos onde deveria haver um.
Esse problema é inerente a operações não idempotentes sobre redes não confiáveis, e uma API bem projetada oferece ao cliente uma forma de se proteger dele: as chaves de idempotência. O cliente gera um identificador único para a operação e o envia num cabeçalho; o servidor registra esse identificador e, se receber uma segunda requisição com a mesma chave, reconhece que é uma repetição e retorna o resultado da primeira em vez de executar a operação de novo. O pedido é criado exatamente uma vez, mesmo que a requisição chegue várias.
Nem toda API precisa disso em todos os endpoints, mas qualquer operação com consequências reais — cobrar um cartão, criar um recurso, disparar um envio — se beneficia enormemente de ser projetada para tolerar repetição segura. É a diferença entre uma API que funciona em demonstrações e uma que sobrevive à produção, onde timeouts, retries e redes intermitentes são o dia a dia, não a exceção. Projetar assumindo que a rede vai falhar não é pessimismo; é a premissa correta.
Paginação, filtragem e os detalhes que escalam#
Uma API que retorna "todos os pedidos" funciona perfeitamente com dez pedidos no banco e desmorona com dez milhões. Coleções precisam de paginação desde o primeiro dia, porque adicioná-la depois é uma mudança que quebra clientes que esperavam a lista completa.
Existem duas abordagens principais, e a escolha tem consequências. A paginação por offset (?pagina=3&tamanho=20) é simples de entender, mas tem dois defeitos em escala: fica lenta em coleções grandes, porque o banco precisa contar e pular todos os registros anteriores, e produz resultados inconsistentes se itens forem inseridos ou removidos entre uma página e outra — um item pode aparecer duas vezes ou sumir. A paginação por cursor (?depois=<cursor>) usa um ponteiro estável para a posição, é eficiente independentemente da profundidade, e permanece consistente sob mudanças concorrentes. Para dados que crescem e mudam, cursor é a escolha mais robusta.
O mesmo cuidado vale para filtragem e ordenação: ofereça parâmetros de query claros e, crucialmente, imponha limites. Um parâmetro de tamanho de página sem teto é um convite a um pedido de "me dê um milhão de registros" que derruba o servidor. A API deve clampar o tamanho a um máximo razoável, silenciosamente, protegendo-se de clientes ingênuos ou maliciosos. Cada parâmetro que aceita influência do cliente sobre uma consulta é também uma superfície que precisa ser validada e limitada.
Documentação e o pacto com quem consome#
Nenhuma API é melhor do que sua documentação, porque a documentação é a interface do ponto de vista de quem integra. Uma especificação formal — no padrão OpenAPI, por exemplo — que descreve cada endpoint, cada parâmetro, cada formato de resposta e cada código de erro, não é um extra opcional: é parte da entrega. Ela permite gerar clientes automaticamente, validar requisições, e serve como a fonte única de verdade sobre o contrato. Uma API sem documentação confiável obriga cada consumidor a fazer engenharia reversa por tentativa e erro, o que multiplica o custo de integração e os chamados de suporte.
O fio que costura tudo isso é a consistência. Uma API onde uns endpoints usam snake_case e outros camelCase, onde umas datas vêm em ISO 8601 e outras em timestamp Unix, onde uns erros são 4xx e outros 200 com corpo de erro, é uma API que trai a confiança do consumidor a cada requisição. Escolha convenções — de nomenclatura, de formato de data, de estrutura de erro, de paginação — e aplique-as sem exceção em toda a superfície. Muitas dessas escolhas de design existem para servir a camada de dados por baixo, e projetar bons endpoints anda de mãos dadas com entender como o banco responde a cada consulta, assunto que aprofundamos em como os índices SQL realmente funcionam. Uma boa API é, no fim, um exercício de empatia com quem vai consumi-la — e a empatia se expressa em previsibilidade.