Sua API faz update em uma chamada de GET?
O verbo DELETE nunca nem apareceu no seu código?

Se sim, respira fundo que esse artigo é pra você. Vamos falar sobre algumas boas práticas para APIs RESTful. E como evitar esses "pecados mortais" que acabam confundindo quem consome sua API.

Monkey mum and son in the monkey forest, Ubud

O objetivo de uma API RESTful é muito parecido com o de um Design Pattern:
oferecer uma solução reutilizável para problemas comuns.

Um Design Pattern existe para acelerar o desenvolvimento com base em abordagens já testadas e amplamente utilizadas. Da mesma forma, o REST traz uma padronização que ajuda a tornar as APIs mais consistentes, previsíveis e fáceis de entender.

Em outras palavras: quando seguimos o padrão REST, o consumo da API fica mais homogêneo, reduzindo o esforço e a curva de aprendizado dos desenvolvedores que vão utilizá-la.

HTTP do jeito certo

Quando falamos em APIs REST, não estamos apenas expondo dados na web, estamos estabelecendo um contrato de comunicação. Esse contrato precisa ser previsível: quem consome a API deve entender claramente o que esperar de cada endpoint.

É aqui que entram os métodos HTTP e os conceitos de Safe e Idempotente. Eles garantem que uma API siga padrões consistentes, facilite a integração e evite comportamentos inesperados.

Safe x Idempotente

  • Safe: métodos somente de leitura, que não alteram o estado do servidor.
  • Idempotente: métodos que, independentemente de quantas vezes sejam chamados com a mesma intenção, produzem o mesmo efeito no estado do servidor (a resposta pode variar, mas o resultado final não muda).

Tabela de métodos

HTTP MethodSafeIdempotenteDescrição
OPTIONSRetorna verbos e opções do resource (ex.: CORS).
GETBusca um resource.
HEADBusca apenas o header do resource.
PUTAtualiza ou substitui um resource por completo.
POSTCria um resource ou executa uma ação não idempotente.
DELETERemove um resource.
PATCHAtualiza parcialmente um resource.

GET

O método GET é Safe e Idempotente. Ele deve apenas consultar dados, nunca criar, atualizar ou remover recursos. O resultado do GET será sempre o mesmo para o mesmo conjunto de informações, sem alterar o estado do servidor.

PUT

O método PUT substitui completamente a representação de um resource. É idempotente, pois várias chamadas com a mesma payload terão o mesmo efeito.

Exemplo:

{
  "nome": "Maria"
}
  • Primeira chamada: altera o nome para "Maria" → 200 OK.
  • Segunda chamada (mesma payload): nada muda, mas a resposta continua sendo 200 OK.

Por alterar estado, não é Safe, mas permanece idempotente.

DELETE

O método DELETE é idempotente, mas não é Safe. Ele remove o resource identificado pela URI. A primeira chamada remove o recurso; chamadas subsequentes não alteram mais nada no servidor, mas continuam válidas (idempotência).

Status comuns

  • 204 No Content: remoção concluída, sem payload.
  • 202 Accepted: remoção aceita, mas processada de forma assíncrona (ex.: filas, jobs).
  • 404 Not Found: o recurso já não existe (mesmo assim, continua idempotente).
  • 410 Gone: o recurso existiu, mas foi removido permanentemente.

Boas práticas

  • Defina claramente se a remoção é hard delete (definitiva) ou soft delete (marcação lógica, ex.: isDeleted=true).
  • Documente efeitos em cascata: caso a remoção impacte recursos relacionados, deve estar explícito (ex.: ?cascade=true).
  • Considere o uso de controle de concorrência com ETag e If-Match para evitar remoções inconsistentes.
  • Sempre exija autenticação e autorização para chamadas de DELETE.

POST

O método POST é usado para criar resources. Cada chamada pode resultar em um novo recurso, por isso não é nem Safe nem Idempotente.

Normalmente retorna 201 Created com o header Location, indicando a URI do recurso criado. Exemplo:

Location: /usuarios/123

PATCH

O método PATCH realiza atualizações parciais. Por padrão não é idempotente, embora possa ser projetado para ser, dependendo da implementação.

Exemplo de atualização parcial:

[
  { "operation": "replace", "field": "email", "value": "[email protected]" }
]

Se regras de negócio diferentes produzirem resultados distintos em chamadas subsequentes (ex.: limite de tempo entre alterações), a operação não será idempotente.

IMPORTANTE: Recurso principal x efeitos colaterais

Os conceitos de Safe e Idempotente se aplicam ao resource principal da requisição, não ao sistema como um todo.

Por exemplo:

Um GET em /faturas/123 não deve alterar a fatura em si (o recurso principal).

Porém, esse mesmo GET pode incrementar um contador de acessos, gerar um log de auditoria ou até mesmo atualizar um sistema de cobrança por consumo da API (billing).

Esses efeitos colaterais são aceitáveis, contanto que o estado do recurso consultado continue inalterado. Ou seja: a fatura continua igual, mesmo que seu acesso tenha sido registrado em outro lugar.

Endpoints

Padrão de nomenclatura

Seguir um padrão consistente de nomenclatura facilita a compreensão e a manutenção da API. Algumas boas práticas:

  • Pluralidade: utilize o plural para disponibilizar um recurso. Exemplo: /users em vez de /user.
  • Kebab-case: para nomes compostos, prefira o formato kebab-case. Exemplo: /global-configuration.
  • Minúsculas: mantenha as URLs em minúsculo, evitando variações como /Users. Prefira sempre /users.

Estrutura de recursos

A raiz de um recurso deve representar uma coleção. Exemplo:

  • GET /users → retorna uma lista de usuários.
  • GET /users/bruno → retorna o usuário de identificador único bruno (não precisa ser o ID do banco, pode ser outro campo único, como username ou um slug).

Operações típicas de CRUD sobre usuários:

  • GET /users → retorna lista de usuários.
  • GET /users/bruno → retorna o usuário bruno.
  • POST /users → cria um novo usuário.
  • PUT /users/bruno → atualiza os dados do usuário bruno.
  • PATCH /users/bruno → atualiza parcialmente o usuário bruno.
  • DELETE /users/bruno → remove o usuário bruno.

Relacionamentos filho

Recursos relacionados devem ser mapeados de forma hierárquica:

  • GET /users/bruno/claims → retorna todas as claims do usuário bruno.
  • GET /users/bruno/claims/6 → retorna a claim de id 6.
  • POST /users/bruno/claims → cria uma nova claim para o usuário bruno.
  • PUT /users/bruno/claims/6 → atualiza a claim 6.
  • PATCH /users/bruno/claims/6 → atualiza parcialmente a claim 6.
  • DELETE /users/bruno/claims/6 → remove a claim 6.

Nem sempre os relacionamentos seguem hierarquia direta.

Por exemplo, um usuário pertence a um departamento, mas pode fazer sentido expor /departamentos e /usuarios como recursos independentes, cada um com sua própria URI.

Ações

Até aqui, os exemplos são casos clássicos de CRUD. No mundo real, no entanto, recursos também possuem comportamentos.

Exemplo: considere o endpoint /candidatos/{id}. Agora é necessário aprovar ou recusar um candidato. Dentro do CRUD tradicional, poderíamos:

  1. Alterar o campo status via PUT /candidatos/{id}.
  2. Usar PATCH /candidatos/{id} para atualizar apenas o campo status.

O problema é que essas operações não traduzem a intenção de negócio. Mudar o status pode significar muito mais do que atualizar um campo:

  • Aprovar um candidato pode envolver notificação ao gestor.
  • Corrigir a grafia de um nome pode gerar reemissão de crachá.

Ambas seriam "atualizações", mas com implicações completamente diferentes.

Clareza com ações específicas

Uma abordagem mais clara é expor ações como endpoints específicos. Isso evita que o backend precise interpretar intenções ocultas em atualizações genéricas.

Exemplos:

POST   /candidatos/{id}/aprovar     -> aprova o candidato
DELETE /candidatos/{id}/aprovar     -> cancela a aprovação

POST   /candidatos/{id}/reprovar    -> reprova o candidato
DELETE /candidatos/{id}/reprovar    -> cancela a reprovação

POST   /candidatos/{id}/transferir  -> transfere o candidato para outra vaga
DELETE /candidatos/{id}/transferir  -> cancela a transferência

Nesse formato, a intenção do endpoint é explícita e facilmente compreensível, sem a necessidade de regras complexas no backend para interpretar o que "atualizar status" realmente significa.

Vale lembrar: expor ações não fere os princípios REST. Pelo contrário, torna a comunicação mais clara, aderente à linguagem ubíqua (DDD) e alinhada ao domínio do negócio.

Pesquisas

É comum ver times criando endpoints de pesquisa via POST, enviando filtros no corpo da requisição. O problema é que o POST não é idempotente (pode gerar efeitos colaterais) e não é Safe (não deveria ser usado apenas para leitura). Por isso, ele não é a escolha ideal para consultas.

O POST foi feito para criação de recursos (associação clássica: POST -> INSERT). Para consultas o verbo correto é o GET, utilizando parâmetros de filtro na URL.

O problema do POST em consultas

Além da semântica incorreta, usar POST para buscas traz outro impacto grave: o cache.

  • Caches HTTP (servidor, navegador e intermediários) não armazenam por padrão resultados de POST.
  • CDNs como Cloudflare, Azure Front Door ou Akamai são otimizadas para cachear GET.
  • Se você usa POST para consultas, cada chamada sempre chega ao backend, aumentando latência e custo (mesmo quando o resultado poderia ser servido do cache).

Isso pode inviabilizar estratégias de escalabilidade que dependem de cache distribuído — justamente um dos pontos fortes de arquiteturas modernas em nuvem.

Exemplos corretos

  • GET /users → retorna uma lista de usuários.
  • GET /users/{id} → retorna um único usuário.

Filtrando

Use query parameters para refinar os resultados:

  • GET /users?status=active&older_than=30

Essas URLs são cacheáveis, o que permite que CDNs armazenem a resposta e melhorem a performance da API globalmente.

Alias para pesquisas complexas

Se a consulta exigir muitos parâmetros, use aliases para simplificar:

  • Complexo:

    GET /users?status=active&lives_in=brazil&older_than=18&younger_than=35
    
  • Melhor com alias:

    GET /users/young-brazilians
    

Além de mais legível, facilita o cache em camadas intermediárias.

Ordenação

Permita ordenação via query string, usando + (ascendente) ou - (descendente):

  • GET /users?sort=firstname,-last_login

Exemplo:

  • firstname → ascendente.
  • last_login → descendente.

Paginação

Implemente paginação para evitar sobrecarregar a API. Convencionalmente:

  • GET /users?limit=10&offset=20 → retorna 10 registros, a partir do 21º.

Essa abordagem mantém a consulta cacheável, diferente de um POST que sempre iria até o backend.

Utilizar ProblemDetails

A RFC 7807 define um formato padrão para respostas de erro em APIs HTTP, chamado Problem Details.

O objetivo é simples: fornecer uma forma consistente e legível de transportar informações sobre erros, sem precisar inventar formatos próprios para cada API.

A grande vantagem está na consistência: quando respostas de erro seguem um padrão, a API se torna mais previsível e fácil de consumir.

Trecho da especificação:

This specification defines simple JSON [RFC7159] and XML [W3C.REC-xml-20081126] document formats to suit this purpose. They are designed to be reused by HTTP APIs, which can identify distinct "problem types" specific to their needs.

Um exemplo de resposta no formato ProblemDetails em JSON:

{
  "type": "https://httpstatuses.com/404",
  "title": "Not Found",
  "status": 404,
  "detail": "O usuário solicitado não foi encontrado.",
  "instance": "/users/123"
}

JSON

Para APIs expostas na internet, prefira JSON como formato de resposta. Embora o XML já tenha sido amplamente utilizado (especialmente em SOAP), ele é verboso, pesado e mais difícil de interpretar.

Atualmente, o JSON é o padrão. XML e SOAP é sistema legado e pano pra manga.

Propriedades null

Evite retornar propriedades null nas respostas JSON. Isso só aumenta o tamanho do payload sem agregar valor.

APIs modernas e bibliotecas de serialização/deserialização sabem lidar com a ausência de campos. Assim, em vez de:

{
  "id": 123,
  "nome": "Bruno",
  "telefone": null
}

Prefira:

{
  "id": 123,
  "nome": "Bruno"
}

HATEOAS

Um dos princípios teóricos do REST é o HATEOAS (Hypermedia as the Engine of Application State). Na prática, a ideia é que a navegação na API seja guiada por links, de forma parecida com a web tradicional.

Exemplo simplificado:

{
  "id": 123,
  "nome": "Bruno",
  "links": [
    { "rel": "self", "href": "/users/123" },
    { "rel": "claims", "href": "/users/123/claims" }
  ]
}

Embora faça sentido no mundo na teoria, no contexto de APIs REST não faz muito sentido. E como sei que sempre vai ter um engraçadinho para dizer: Mas lá onde trampo... Então: A exceção comprova a regra.

Conclusão

Neste post compartilhei um conjunto de boas práticas para o design de APIs RESTful, fruto de experiências acumuladas ao longo dos anos em diferentes projetos. Embora não exista uma RFC única ou guia oficial definitivo sobre o tema, seguir padrões consistentes torna qualquer API mais previsível, fácil de usar e de manter.

As práticas aqui descritas não são regras absolutas, mas recomendações que todo mundo usa.

Referências

Algumas fontes que também reforçam e complementam as boas práticas apresentadas: