Padrões de contrato de erros de API para mensagens claras e amigáveis
Projete um contrato de erro de API com códigos estáveis, mensagens localizadas e dicas amigáveis à UI que reduzem a carga do suporte e ajudam usuários a se recuperar rapidamente.

Por que erros vagos de API criam problemas reais para usuários
Um erro vago de API não é apenas um incômodo técnico. É um momento quebrado no produto em que alguém fica travado, tenta adivinhar o que fazer a seguir e, muitas vezes, desiste. Aquela única mensagem "Algo deu errado" se transforma em mais tickets de suporte, churn e bugs que nunca parecem totalmente resolvidos.
Um padrão comum é este: um usuário tenta salvar um formulário, a interface mostra um aviso genérico e os logs do backend indicam a causa real ("violação de restrição única no email"). O usuário não sabe o que alterar. O suporte não consegue ajudar porque não há um código confiável para buscar nos logs. O mesmo problema é reportado com diferentes capturas de tela e descrições, e não há uma maneira limpa de agrupar os casos.
Detalhes para desenvolvedores e necessidades dos usuários não são a mesma coisa. Engenheiros precisam de contexto preciso da falha (qual campo, qual serviço, qual timeout). Usuários precisam de um passo claro a seguir: "Este email já está em uso. Tente fazer login ou use outro email." Misturar esses dois geralmente leva a divulgação insegura (vazamento de internos) ou mensagens inúteis (esconder tudo).
É para isso que serve um contrato de erro de API. O objetivo não é "mais erros", mas uma estrutura consistente para que:
- clientes possam interpretar falhas de forma confiável entre endpoints
- usuários vejam mensagens seguras e em linguagem simples que os ajudem a resolver o problema
- suporte e QA identifiquem o problema exato com um código estável
- engenheiros obtenham diagnósticos sem expor detalhes sensíveis
Consistência é o ponto central. Se um endpoint retorna error: "Invalid" e outro retorna message: "Bad request", a UI não consegue orientar usuários e a equipe não consegue mensurar o que está acontecendo. Um contrato claro torna erros previsíveis, pesquisáveis e mais fáceis de consertar, mesmo quando as causas subjacentes mudam.
O que um contrato de erro consistente significa na prática
Um contrato de erro de API é uma promessa: quando algo dá errado, sua API responde com uma forma familiar, com campos e códigos previsíveis, independentemente de qual endpoint falhou.
Não é um despejo de debug, nem um substituto para logs. O contrato é aquilo em que apps clientes podem confiar com segurança. Logs são onde você guarda stack traces, detalhes SQL e qualquer coisa sensível.
Na prática, um contrato sólido mantém algumas coisas estáveis: o formato da resposta entre endpoints (para 4xx e 5xx), códigos de erro legíveis por máquina que não mudam de significado e uma mensagem segura para o usuário. Também ajuda o suporte ao incluir um identificador de requisição/trace, e pode incluir dicas simples para a UI, como se o usuário deve tentar novamente ou corrigir um campo.
A consistência só funciona se você decidir onde ela será aplicada. Equipes costumam começar com um ponto de aplicação e expandir depois: um API gateway que normaliza erros, middleware que envolve falhas não tratadas, uma biblioteca compartilhada que constrói o mesmo objeto de erro, ou um handler de exceção no nível do framework por serviço.
A expectativa-chave é simples: todo endpoint retorna ou uma forma de sucesso ou o contrato de erro para cada modo de falha. Isso inclui erros de validação, falhas de autenticação, limites de taxa, timeouts e falhas em sistemas upstream.
Um formato simples de resposta de erro que escala
Um bom contrato de erro de API permanece pequeno, previsível e útil para pessoas e software. Quando um cliente sempre encontra os mesmos campos, o suporte para de adivinhar e a UI pode oferecer ajuda mais clara.
Aqui está um formato JSON mínimo que funciona para a maioria dos produtos (e escala conforme você adiciona mais endpoints):
{
"status": 400,
"code": "AUTH.INVALID_EMAIL",
"message": "Enter a valid email address.",
"details": {
"fields": {
"email": "invalid_email"
},
"action": "fix_input",
"retryable": false
},
"trace_id": "01HZYX8K9Q2..."
}
Para manter o contrato estável, trate cada parte como uma promessa separada:
statusé para comportamento HTTP e categorias amplas.codeé o identificador estável e legível por máquina (o núcleo do seu contrato de erro de API).messageé o texto seguro para a UI (e algo que você pode localizar depois).detailscontém dicas estruturadas: problemas por campo, o que fazer a seguir e se faz sentido tentar novamente.trace_idpermite que o suporte encontre a falha exata no servidor sem expor internos.
Mantenha o conteúdo destinado ao usuário separado de informações internas de depuração. Se for necessário mais diagnóstico, registre-o no servidor associado ao trace_id (não na resposta). Isso evita vazar dados sensíveis enquanto ainda facilita a investigação de problemas.
Para erros de campo, details.fields é um padrão simples: chaves correspondem aos nomes dos inputs e valores contêm razões curtas como invalid_email ou too_short. Adicione orientação só quando ela ajudar. Para timeouts, action: "retry_later" é suficiente. Para quedas temporárias, retryable: true ajuda os clientes a decidir se devem mostrar um botão de tentar novamente.
Uma observação antes de implementar: algumas equipes envolvem erros em um objeto error (por exemplo, { "error": { ... } }) enquanto outras mantêm os campos no nível superior. Qualquer abordagem pode funcionar. O que importa é escolher um envelope e mantê-lo consistente em todos os lugares.
Códigos de erro estáveis: padrões que não quebram clientes
Códigos de erro estáveis são a espinha dorsal de um contrato de erro de API. Eles permitem que apps, painéis e equipes de suporte reconheçam um problema mesmo quando você muda a redação, adiciona campos ou melhora a UI.
Uma convenção prática de nomes é:
DOMÍNIO.AÇÃO.MOTIVO
Exemplos: AUTH.LOGIN.INVALID_PASSWORD, BILLING.PAYMENT.CARD_DECLINED, PROFILE.UPDATE.EMAIL_TAKEN. Mantenha domínios pequenos e familiares (AUTH, BILLING, FILES). Use verbos de ação que leiam claramente (CREATE, UPDATE, PAY).
Trate códigos como endpoints: uma vez públicos, não devem mudar de significado. O texto mostrado ao usuário pode melhorar com o tempo (tom melhor, passos mais claros, novos idiomas), mas o código deve permanecer o mesmo para que clientes não quebrem e a análise permaneça limpa.
Também vale decidir quais códigos são públicos versus internos. Uma regra simples: códigos públicos devem ser seguros para exibir, estáveis, documentados e usados pela UI. Códigos internos pertencem aos logs para depuração (nomes de banco de dados, detalhes de fornecedor, stack info). Um código público pode mapear para muitas causas internas, especialmente quando uma dependência pode falhar de várias maneiras.
Deprecação funciona melhor quando é entediante. Se precisar substituir um código, não o reutilize silenciosamente com um novo significado. Introduza um novo código e marque o antigo como deprecated. Dê uma janela de sobreposição onde ambos podem aparecer. Se incluir um campo como deprecated_by, aponte para o novo código (não uma URL).
Por exemplo, mantenha BILLING.PAYMENT.CARD_DECLINED mesmo que depois melhore a copy da UI e a divida entre "Tente outro cartão" vs "Ligue para seu banco". O código permanece estável enquanto a orientação evolui.
Mensagens localizadas sem perder consistência
Localização complica quando a API retorna frases completas e clientes as tratam como lógica. Uma abordagem melhor é manter o contrato estável e traduzir o texto na última etapa. Assim, o mesmo erro significa a mesma coisa independentemente do idioma do usuário, dispositivo ou versão do app.
Primeiro, decida onde as traduções vivem. Se precisar de uma fonte única para web, mobile e ferramentas de suporte, mensagens no servidor podem ajudar. Se a UI precisa de controle fino sobre tom e layout, traduções no cliente costumam ser mais fáceis. Muitas equipes usam um híbrido: a API retorna um código estável mais uma chave de mensagem e parâmetros, e o cliente escolhe o melhor texto a exibir.
Para um contrato de erro de API, chaves de mensagem são geralmente mais seguras do que sentenças codificadas. A API pode retornar algo como message_key: "auth.too_many_attempts" com params: {"retry_after_seconds": 300}. A UI traduz e formata isso sem mudar o significado.
Pluralização e fallbacks importam mais do que as pessoas esperam. Use uma solução i18n que suporte regras de plural por localidade, não apenas o estilo inglês "1 vs muitos". Defina uma cadeia de fallback (por exemplo: fr-CA -> fr -> en) para que strings ausentes não virem telas em branco.
Uma boa diretriz é tratar texto traduzido estritamente como conteúdo para o usuário. Não coloque stack traces, IDs internos ou detalhes brutos do "porquê falhou" em strings localizadas. Mantenha detalhes sensíveis em campos não exibidos (ou nos logs) e dê aos usuários redações seguras e acionáveis.
Transformando falhas do backend em dicas de UI que usuários seguem
A maioria das falhas do backend é útil para engenheiros, mas com muita frequência acaba na tela como "Algo deu errado". Um bom contrato de erro transforma falhas em passos claros sem vazar detalhes sensíveis.
Uma abordagem simples é mapear falhas para uma de três ações do usuário: corrigir input, tentar novamente ou contatar o suporte. Isso mantém a UI consistente entre web e mobile mesmo quando o backend tem muitos modos de falha.
- Corrigir input: validação falhou, formato incorreto, campo obrigatório ausente.
- Tentar novamente: timeouts, problemas temporários upstream, limites de taxa.
- Contatar suporte: questões de permissão, conflitos que o usuário não resolve, erros internos inesperados.
Dicas por campo importam mais que mensagens longas. Quando o backend sabe qual input falhou, retorne um apontador legível por máquina (por exemplo, um nome de campo como email ou card_number) e uma razão curta que a UI pode mostrar inline. Se vários campos estiverem errados, retorne todos para que o usuário corrija de uma vez.
Também ajuda combinar o padrão de UI com a situação. Um toast é aceitável para uma mensagem temporária de retry. Erros de input devem ser inline. Bloqueadores de conta e pagamento geralmente exigem um diálogo bloqueante.
Inclua contexto de troubleshooting de forma consistente e segura: trace_id, um timestamp se já existir, e um passo sugerido como um tempo de espera para retry. Assim, um timeout do provedor de pagamento pode mostrar "Serviço de pagamento lento. Por favor, tente novamente" com um botão de retry, enquanto o suporte usa o mesmo trace_id para encontrar a falha no servidor.
Passo a passo: implante o contrato de ponta a ponta
Implantar um contrato de erro de API funciona melhor quando você trata isso como uma pequena mudança de produto, não um refactor. Avance incrementalmente e envolva suporte e equipes de UI cedo.
Uma sequência de rollout que melhora mensagens para o usuário rapidamente sem quebrar clientes:
- Faça o inventário do que já existe (agrupando por domínio). Exporte respostas reais de erro dos logs e agrupe em categorias como auth, signup, billing, upload de arquivos e permissões. Procure repetições, mensagens confusas e lugares onde a mesma falha aparece em cinco formatos diferentes.
- Defina o esquema e compartilhe exemplos. Documente o formato da resposta, campos obrigatórios e exemplos por domínio. Inclua nomes de códigos estáveis, uma chave de mensagem para localização e uma seção opcional de dicas para a UI.
- Implemente um mapeador de erros central. Coloque a formatação em um único ponto para que todo endpoint retorne a mesma estrutura. Em um backend gerado (ou um backend no-code como AppMaster), isso frequentemente significa um passo compartilhado "mapear erro para resposta" que cada endpoint ou processo de negócio chama.
- Atualize a UI para interpretar códigos e mostrar dicas. Faça a UI depender de códigos, não do texto da mensagem. Use os códigos para decidir se destaca um campo, mostra ação de retry ou sugere contatar suporte.
- Adicione logging e um trace_id que o suporte possa pedir. Gere um trace_id para cada requisição, registre-o no servidor com detalhes brutos da falha e retorne-o na resposta de erro para que usuários possam copiá-lo.
Após a primeira implementação, mantenha o contrato estável com alguns artefatos leves: um catálogo compartilhado de códigos de erro por domínio, arquivos de tradução para mensagens localizadas, uma tabela simples de mapeamento código -> dica da UI / próxima ação, e um playbook de suporte que comece com "envie-nos seu trace_id".
Se tiver clientes legados, mantenha campos antigos por uma curta janela de depreciação, mas pare de criar formatos únicos imediatamente.
Erros comuns que tornam mais difícil dar suporte
A maior parte da dor do suporte não vem de "usuários ruins". Vem da ambiguidade. Quando seu contrato de erro de API é inconsistente, cada time inventa sua própria interpretação e usuários ficam com mensagens sem ação.
Uma armadilha comum é tratar códigos de status HTTP como toda a história. "400" ou "500" diz quase nada sobre o que o usuário deve fazer a seguir. Status ajudam no transporte e na classificação ampla, mas você ainda precisa de um código estável em nível de aplicação que mantenha seu significado entre versões.
Outro erro é mudar o significado de um código ao longo do tempo. Se PAYMENT_FAILED significava "cartão recusado" e depois passa a significar "Stripe indisponível", sua UI e docs ficam incorretos sem que ninguém perceba. O suporte então recebe tickets como "Tentei três cartões e continua a falhar" quando o real problema é um outage.
Retornar texto bruto de exceção (ou pior, stack traces) é tentador por ser rápido. Raramente é útil para usuários e pode vazar detalhes internos. Mantenha diagnósticos brutos nos logs, não nas respostas exibidas às pessoas.
Alguns padrões geram ruído consistentemente:
- Usar demais um código catch-all como
UNKNOWN_ERRORelimina qualquer chance de orientar o usuário. - Criar muitos códigos sem uma taxonomia clara torna dashboards e playbooks difíceis de manter.
- Misturar texto para usuário com diagnósticos de desenvolvedor no mesmo campo torna localização e dicas de UI frágeis.
Uma regra simples ajuda: um código estável por decisão do usuário. Se o usuário pode consertar mudando um input, use um código específico e uma dica clara. Se não pode (como em outage de provedor), mantenha o código estável e retorne uma mensagem segura mais uma ação como "Tente novamente mais tarde" e um ID de correlação para o suporte.
Checklist rápido antes do lançamento
Antes de enviar, trate erros como um recurso de produto. Quando algo falhar, o usuário deve saber o que fazer a seguir, o suporte deve conseguir encontrar o evento exato, e clientes não devem quebrar quando o backend mudar.
- Mesmo formato em todos os lugares: todo endpoint (incluindo auth, webhooks e uploads) retorna um envelope de erro consistente.
- Códigos estáveis e responsáveis: cada código tem um dono claro (Payments, Auth, Billing). Não reutilize um código para um significado diferente.
- Mensagens seguras e localizáveis: texto para usuário curto e sem segredos (tokens, dados completos de cartão, SQL cru, stack traces).
- Próxima ação clara na UI: para os tipos de falha mais comuns, a UI mostra um passo óbvio (tente novamente, atualize um campo, use outro método de pagamento, contate suporte).
- Rastreabilidade para suporte: toda resposta de erro inclui um
trace_id(ou similar) que o suporte pode pedir, e seus logs/monitoramento encontram a história completa rapidamente.
Teste alguns fluxos realistas de ponta a ponta: um formulário com input inválido, uma sessão expirada, um limite de taxa e uma queda de terceiro. Se você não consegue explicar a falha em uma frase e apontar o trace_id exato nos logs, não está pronto para lançar.
Exemplo: falhas de cadastro e pagamento que usuários conseguem recuperar
Um bom contrato de erro de API torna a mesma falha compreensível em três lugares: sua UI web, seu app mobile e o email automatizado que o sistema pode enviar após uma tentativa frustrada. Também dá ao suporte detalhes suficientes para ajudar sem pedir que o usuário envie capturas de tela.
Cadastro: erro de validação que o usuário pode corrigir
Um usuário digita um email como sam@ e toca em Cadastrar. A API retorna um código estável e uma dica por campo, para que todos os clientes destaquem o mesmo input.
{
"error": {
"code": "AUTH.EMAIL_INVALID",
"message": "Enter a valid email address.",
"i18n_key": "auth.email_invalid",
"params": { "field": "email" },
"ui": { "field": "email", "action": "focus" },
"trace_id": "4f2c1d..."
}
}
Na web, você mostra a mensagem abaixo do campo de email. No mobile, você foca o campo de email e exibe um pequeno banner. No email, pode dizer: "Não conseguimos criar sua conta porque o endereço de email parece incompleto." Mesmo código, mesmo significado.
Pagamento: falha com explicação segura
Um pagamento com cartão falha. O usuário precisa de orientação, mas você não deve expor detalhes do processador. Seu contrato pode separar o que o usuário vê do que o suporte pode verificar.
{
"error": {
"code": "PAYMENT.DECLINED",
"message": "Your payment was declined. Try another card or contact your bank.",
"i18n_key": "payment.declined",
"params": { "retry_after_sec": 0 },
"ui": { "action": "show_payment_methods" },
"trace_id": "b9a0e3..."
}
}
O suporte pode pedir o trace_id e verificar qual código estável foi retornado, se a recusa é final ou retryable, qual conta e valor pertenciam à tentativa, e se a dica de UI foi enviada.
É aqui que um contrato de erro de API compensa: seu web, iOS/Android e fluxos de email permanecem consistentes mesmo quando o provedor de backend ou detalhes internos mudam.
Teste e monitoramento do contrato de erro ao longo do tempo
Um contrato de erro de API não está "feito" quando é lançado. Está quando o mesmo código de erro conduz consistentemente à mesma ação do usuário, mesmo após meses de refatorações e novas features.
Comece testando externamente, como um cliente real. Para cada código de erro que você suporta, escreva ao menos uma requisição que o dispare e afirme o comportamento do qual depende: status HTTP, código, chave de localização e campos de dica da UI (como qual campo do formulário destacar).
Um conjunto pequeno de testes cobre a maior parte do risco:
- um pedido happy-path ao lado de cada caso de erro (para pegar validações excessivas acidentais)
- um teste por código estável para checar campos de dica ou mapeamento de campo
- um teste que garante que falhas desconhecidas retornam um código genérico seguro
- um teste que garante que chaves de localização existem para cada idioma suportado
- um teste que assegura que detalhes sensíveis nunca aparecem em respostas ao cliente
Monitoramento é como você pega regressões que testes não detectam. Acompanhe contagens de códigos de erro ao longo do tempo e alerte em picos súbitos (por exemplo, um código de pagamento dobrando após um release). Também vigie por novos códigos em produção. Se um código aparecer e não estiver na sua lista documentada, alguém provavelmente contornou o contrato.
Decida cedo o que permanece interno versus o que vai para os clientes. Uma divisão prática é: clientes recebem um código estável, uma chave de localização e uma dica de ação para o usuário; logs recebem a exceção bruta, stack trace, request ID e falhas de dependências (database, provedor de pagamento, gateway de email).
Uma vez por mês, revise erros usando conversas reais do suporte. Pegue os cinco códigos com maior volume e leia algumas tickets para cada um. Se usuários continuam fazendo a mesma pergunta, a dica da UI está faltando um passo ou a mensagem é ambígua.
Próximos passos: aplique o padrão no seu produto e fluxos
Comece onde a confusão custa mais: etapas com maior taxa de abandono (frequentemente cadastro, checkout ou upload de arquivos) e os erros que geram mais tickets. Padronize esses primeiro para ver impacto em um sprint.
Uma maneira prática de manter o rollout focado é:
- escolha os 10 erros que mais geram suporte e atribua códigos estáveis e defaults seguros
- defina mapeamentos código -> dica da UI -> próxima ação por superfície (web, mobile, admin)
- torne o contrato padrão para novos endpoints e trate campos faltantes como motivo de revisão
- mantenha um pequeno playbook interno: o que cada código significa, o que o suporte pede e quem é o dono das correções
- acompanhe algumas métricas: taxa de erro por código, contagem de "erro desconhecido" e volume de tickets atrelado a cada código
Se estiver construindo com AppMaster (appmaster.io), vale integrar isso cedo: defina um formato de erro consistente para seus endpoints e mapeie códigos estáveis para mensagens da UI nas suas telas web e mobile para que usuários recebam o mesmo significado em todos os lugares.
Um exemplo simples: se o suporte continua recebendo reclamações de "Pagamento falhou", padronizar permite que a UI mostre "Cartão recusado" com uma dica para tentar outro cartão para um código, e "Sistema de pagamento temporariamente indisponível" com uma ação de retry para outro código. O suporte pode pedir o trace_id em vez de chutar a causa.
Coloque uma limpeza periódica no calendário. Aposente códigos não usados, aperfeiçoe mensagens vagas e adicione texto localizado onde houver volume real. O contrato permanece estável enquanto o produto continua mudando.


