Design de respostas JSON em APIs REST: boas práticas e antipadrões
Os bytes que uma API REST retorna importam tanto quanto as rotas e os códigos de status. Todo cliente que chamar sua API é escrito contra o formato do seu JSON, e remodelar esse JSON depois quebra esses clientes, por menor que a mudança pareça por dentro. Este guia cobre as decisões de design de resposta que custam pouco para acertar no início e muito para corrigir quando já existem clientes.
Escolha um formato de envelope e nunca dele desvie
Um envelope é o wrapper consistente de nível superior que toda resposta usa, independente do endpoint: um conjunto conhecido de chaves como `data`, `error` e `meta` que a camada HTTP de um cliente consegue tratar genericamente, em vez de escrever parsing específico por endpoint. Sem um, todo endpoint que retorna um formato ligeiramente diferente obriga todo cliente a escrever um parser ligeiramente diferente.
// Success
{
"data": { "id": 42, "name": "Ada Lovelace" },
"meta": { "requestId": "a1b2c3" }
}
// Failure
{
"data": null,
"error": { "code": "NOT_FOUND", "message": "User 42 does not exist" },
"meta": { "requestId": "a1b2c4" }
}Os nomes das chaves importam menos do que a consistência: o que você escolher, todo endpoint retorna, `data` é sempre onde o payload vive, e um cliente pode escrever uma única função `unwrap(response)` para a API inteira em vez de uma por recurso.
Nunca retorne um array (ou escalar) puro no nível superior
Um `GET /users` que retorna um array JSON puro (`[{...}, {...}]`) parece natural, mas você não conseguirá adicionar metadados no nível da resposta sem uma breaking change. Quando precisar de uma contagem total, um cursor de próxima página ou um aviso junto com a lista, terá de mudar o tipo de nível superior de array para objeto, e isso quebra todo cliente que faz `response.map(...)` ou `response.length` sobre o corpo analisado.
Envolva coleções num campo nomeado desde o primeiro dia (`{ "users": [...] }` ou `{ "data": [...] }`), mesmo que você tenha certeza de que nunca vai precisar de metadados extras. Adicionar uma chave irmã a um objeto mantém a compatibilidade. Transformar um array em objeto quebra qualquer cliente, em qualquer linguagem.
Null vs. omitido: não são o mesmo sinal
Um campo presente com valor `null` e um campo ausente da resposta parecem 'sem valor' à primeira vista, mas significam coisas diferentes, e os clientes precisam distingui-los. `null` diz que o campo é um conceito conhecido deste recurso e que seu valor atual é nada (um usuário sem `middleName`). Omitido diz que o campo não se aplica, não foi carregado ou não foi solicitado (uma requisição de campos esparsos que pediu só `id` e `name`).
A semântica do PATCH depende dessa distinção. No JSON Merge Patch (RFC 7396), enviar um campo como `null` apaga esse campo do destino, enquanto deixar o campo fora do corpo do patch o mantém inalterado. Se você confundir os dois, ou deixar vago quais campos podem ser `null`, os clientes não conseguem distinguir 'limpe este valor' de 'não enviei atualização para este campo'.
Paginação: cursor vs. offset, e como cada uma aparece em JSON
Paginação por offset/limit (`?offset=40&limit=20`, ou `?page=3&pageSize=20`) é a mais simples de implementar e raciocinar, mas tem um problema real de correção: se linhas forem inseridas ou excluídas entre as requisições de página, os offsets deslocam por baixo do cliente, causando linhas puladas ou duplicadas. Ela também fica cara em SQL em offsets profundos, já que o banco ainda precisa percorrer e descartar todas as linhas puladas.
A paginação por cursor entrega ao cliente um token opaco apontando para 'a linha seguinte à última que você viu' em vez de uma posição numérica, então os resultados ficam estáveis quando os dados mudam e as consultas continuam baratas em qualquer profundidade. Em troca, os clientes não conseguem pular para um número de página arbitrário. Eles só avançam (e, se houver suporte, retrocedem) a partir de um cursor.
{
"data": [ { "id": 101 }, { "id": 102 } ],
"meta": {
"nextCursor": "eyJpZCI6MTAyfQ==",
"hasMore": true
}
}Use paginação por cursor como padrão para dados que mudam com frequência (feeds, logs, fluxos de eventos) ou tabelas grandes. A paginação por offset serve para coleções pequenas e quase estáticas em que pular para a página N ajuda (uma tabela administrativa com seletor de páginas).
Objetos de erro e convenções de nomenclatura
Uma resposta de erro deve carregar mais do que um código de status HTTP e uma string: um `code` estável e legível por máquina no qual o cliente possa ramificar com segurança (`"VALIDATION_ERROR"`, não uma frase), uma `message` legível por humanos destinada a logs e desenvolvedores, e, para falhas de validação, um array `details` indicando exatamente quais campos falharam e por quê.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request failed validation",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be at least 0" }
]
}
}Para nomear chaves, camelCase e snake_case funcionam igualmente bem. O estrago vem de misturá-las entre endpoints da mesma API, porque cada cliente passa a precisar de uma lógica de mapeamento diferente por endpoint. Consumidores de JavaScript/TypeScript tendem a preferir camelCase, que combina com a desestruturação nativa; ecossistemas de Python e Ruby costumam preferir snake_case. Escolha um para a API inteira e aplique-o também aos booleanos (`isActive`/`hasAccess`, não uma mistura de `active` e `has_access`).
Antipadrões comuns
JSON stringificado dentro de um campo JSON (`"metadata": "{\"plan\":\"pro\"}"` em vez de `"metadata": { "plan": "pro" }`) obriga todo cliente a analisar duas vezes e joga fora a segurança de tipos sem benefício algum. Em geral indica que o servidor serializou tal como estava uma coluna blob do banco em vez de decodificá-la antes de responder.
Formatos de data inconsistentes (alguns campos como timestamps Unix, outros como `"08/15/2026"`, outros como ISO 8601 completo) forçam os clientes a analisar datas campo a campo em vez de usar um único handler compartilhado. Padronize em ISO 8601 em UTC com offset explícito (`"2026-08-15T09:30:00Z"`) em toda parte.
Vazar campos internos do banco significa retornar artefatos de ORM como `password_hash`, chaves estrangeiras internas destinadas a outro serviço, ou campos de contabilidade (`__v`, `_id`, `created_by_worker_id`) tal como estão numa resposta de API. Mapeie seu modelo de banco para um DTO de resposta explícito em vez de serializar o modelo. Um DTO é a forma confiável de impedir que uma mudança de schema interna vire uma mudança na API pública.
FAQ
- Respostas de erro devem usar o mesmo envelope das respostas de sucesso?
- Sim. Se as respostas de sucesso são `{ "data": ..., "meta": ... }`, as de erro devem manter esse formato de nível superior com `data: null` e um objeto `error` preenchido. Assim o código de tratamento de resposta do cliente verifica uma só coisa, a presença de `error`, sem ramificar pelo formato da resposta a cada código de status.
- camelCase ou snake_case: a escolha importa?
- A convenção em si importa pouco; a consistência importa muito. Escolha uma convenção para a API inteira com base no ecossistema dos seus principais consumidores, documente-a e aplique-a em toda parte, incluindo prefixos booleanos e chaves de objetos aninhados. O custo aparece numa API em que cada endpoint usa uma convenção diferente e todo cliente precisa de lógica de mapeamento específica por endpoint.
- Uma API nova deve usar paginação por cursor ou por offset?
- Padronize na paginação por cursor, a menos que você especificamente precise permitir que usuários saltem para um número de página arbitrário em dados pequenos e majoritariamente estáticos. A paginação por cursor permanece correta quando linhas são adicionadas ou removidas no meio da paginação e continua barata em qualquer profundidade; a paginação por offset degrada em correção e desempenho à medida que o conjunto de dados cresce ou muda.
- Por que não serializar meu modelo de banco diretamente como resposta da API?
- Seu schema de banco e seu contrato de API pública mudam por razões diferentes e em ritmos diferentes. Se você serializa o modelo diretamente, qualquer migração, coluna adicionada ou upgrade de ORM pode mudar ou vazar o formato da sua resposta pública sem ninguém perceber. Um DTO de resposta explícito desacopla os dois, ao custo de uma etapa extra de mapeamento por endpoint.
Experimente estas ferramentas
JSON Validator →
Validate JSON against schemas with detailed error reporting. Generate schemas from JSON.
JSON Formatter →
Format, validate, and minify JSON instantly in your browser, no signup.
JSON Fake Data Generator →
Generate realistic mock JSON, CSV, SQL, and TypeScript datasets dynamically with custom schemas and 18+ field generators.
Artigos relacionados
O que é JSON? Um guia completo para iniciantes →
JSON (JavaScript Object Notation) é um formato leve de troca de dados, fácil de ler por humanos e de analisar por máquinas. Aprenda a sintaxe, os tipos de dados e a estrutura que sustentam as APIs modernas.
A história do JSON: de 2000 a padrão da indústria →
Acompanhe o JSON desde a ideia de Douglas Crockford em 2001, passando pela adoção pelo Yahoo, até se tornar o padrão ECMA-404 que sustenta 90% das APIs modernas.
O guia completo do JSON Schema (Draft 7) →
O JSON Schema é o padrão para descrever e validar a estrutura de JSON. Aprenda as palavras-chave essenciais, construa um schema real de API e aplique as melhores práticas.