Como estruturar erros de domínio vs erros de infraestrutura em APIs
1. Fundamentos da separação entre erros de domínio e infraestrutura
Em APIs modernas, a distinção entre erros de domínio e erros de infraestrutura é essencial para construir sistemas resilientes e com boa experiência para desenvolvedores. Erros de domínio representam violações das regras de negócio — situações esperadas e previstas no fluxo da aplicação, como saldo insuficiente, CPF inválido ou produto fora de estoque. Já erros de infraestrutura são falhas técnicas imprevistas, como banco de dados offline, timeout de rede ou disco cheio.
Essa separação impacta diretamente a estratégia de tratamento de erros: erros de domínio devem retornar códigos HTTP 4xx (indicando problema na requisição ou no estado do negócio), enquanto erros de infraestrutura devem retornar 5xx (indicando falha no servidor). Além disso, a forma de log e monitoramento difere radicalmente: erros de domínio são esperados e podem ser registrados como warning, enquanto erros de infraestrutura exigem alertas imediatos.
2. Modelagem de erros de domínio com tipos e classes específicas
A modelagem começa com classes tipadas que representem cada violação de regra de negócio. Cada classe deve conter código único, mensagem amigável e metadados relevantes.
class InsufficientFundsError {
constructor(accountId, currentBalance, requestedAmount) {
this.code = "INSUFFICIENT_FUNDS";
this.message = "Saldo insuficiente para realizar a transação";
this.details = {
accountId,
currentBalance,
requestedAmount,
deficit: requestedAmount - currentBalance
};
this.type = "DOMAIN_ERROR";
}
}
class InvalidCpfError {
constructor(cpf, reason) {
this.code = "INVALID_CPF";
this.message = "CPF informado não é válido";
this.details = { cpf, reason };
this.type = "DOMAIN_ERROR";
}
}
class ProductOutOfStockError {
constructor(productId, requestedQuantity, availableQuantity) {
this.code = "PRODUCT_OUT_OF_STOCK";
this.message = "Produto sem estoque suficiente";
this.details = { productId, requestedQuantity, availableQuantity };
this.type = "DOMAIN_ERROR";
}
}
3. Modelagem de erros de infraestrutura com abstração técnica
Erros de infraestrutura devem encapsular exceções de bibliotecas externas, traduzindo-as para um formato padronizado sem vazar detalhes técnicos.
class DatabaseConnectionError {
constructor(originalError, operation) {
this.code = "DATABASE_CONNECTION_FAILED";
this.message = "Serviço de banco de dados temporariamente indisponível";
this.details = { operation };
this.internalError = originalError; // apenas para log interno
this.type = "INFRASTRUCTURE_ERROR";
this.statusCode = 503;
}
}
class ServiceTimeoutError {
constructor(serviceName, timeoutMs) {
this.code = "SERVICE_TIMEOUT";
this.message = `O serviço ${serviceName} não respondeu dentro do tempo limite`;
this.details = { serviceName, timeoutMs };
this.type = "INFRASTRUCTURE_ERROR";
this.statusCode = 504;
}
}
// Exemplo de captura e conversão
function executeQuery(query) {
try {
return databaseClient.query(query);
} catch (error) {
if (error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
throw new DatabaseConnectionError(error, "query");
}
throw new DatabaseConnectionError(error, "unknown");
}
}
4. Middleware de tratamento unificado e mapeamento para respostas HTTP
Um middleware central captura todos os erros lançados e os mapeia para respostas HTTP padronizadas.
function errorHandlerMiddleware(error, req, res, next) {
// Mapeamento de erros de domínio para 4xx
if (error.type === "DOMAIN_ERROR") {
const statusMap = {
"INSUFFICIENT_FUNDS": 422,
"INVALID_CPF": 400,
"PRODUCT_OUT_OF_STOCK": 409,
"DUPLICATE_TRANSACTION": 409
};
const statusCode = statusMap[error.code] || 400;
return res.status(statusCode).json({
error: {
code: error.code,
message: error.message,
details: error.details || {}
}
});
}
// Mapeamento de erros de infraestrutura para 5xx
if (error.type === "INFRASTRUCTURE_ERROR") {
const statusCode = error.statusCode || 500;
// Log completo com stack trace
console.error({
type: "INFRASTRUCTURE_ERROR",
code: error.code,
message: error.message,
stack: error.internalError?.stack,
requestId: req.correlationId
});
return res.status(statusCode).json({
error: {
code: error.code,
message: "Serviço temporariamente indisponível. Tente novamente mais tarde."
}
});
}
// Erro desconhecido (fallback)
console.error("UNKNOWN_ERROR", error);
return res.status(500).json({
error: {
code: "INTERNAL_SERVER_ERROR",
message: "Ocorreu um erro inesperado"
}
});
}
5. Logging e observabilidade: diferenciando o que registrar
A estratégia de logging deve refletir a natureza de cada tipo de erro.
// Erros de domínio: log como warning
function logDomainError(error, req) {
logger.warn({
type: "DOMAIN_ERROR",
code: error.code,
message: error.message,
details: error.details,
correlationId: req.correlationId,
userId: req.user?.id,
path: req.path
});
}
// Erros de infraestrutura: log como error com alerta
function logInfrastructureError(error, req) {
logger.error({
type: "INFRASTRUCTURE_ERROR",
code: error.code,
message: error.message,
stack: error.internalError?.stack,
correlationId: req.correlationId,
service: process.env.SERVICE_NAME,
timestamp: new Date().toISOString()
});
// Disparar alerta para equipe de infraestrutura
alertService.sendAlert({
severity: "critical",
error: error.code,
service: process.env.SERVICE_NAME
});
}
6. Estratégias de versionamento e evolução dos erros
Para evoluir os erros sem quebrar contratos, utilize envelopes padronizados e documente mudanças.
// Envelope padrão de erro (versão 1)
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Saldo insuficiente para realizar a transação",
"details": {
"currentBalance": 100.00,
"requestedAmount": 200.00
}
}
}
// Evolução para versão 2 (adicionando campo version)
{
"error": {
"version": "2.0",
"code": "INSUFFICIENT_FUNDS",
"message": "Saldo insuficiente para realizar a transação",
"details": {
"currentBalance": 100.00,
"requestedAmount": 200.00,
"currency": "BRL"
},
"helpUrl": "https://api.docs.com/errors/INSUFFICIENT_FUNDS"
}
}
// Depreciação de código de erro
// Documentar no changelog da API:
// "O código INSUFICIENT_FUNDS será descontinuado na versão 3.0. Use INSUFFICIENT_FUNDS."
7. Testes e validação da estrutura de erros
Testes unitários e de integração garantem que a estrutura de erros funcione corretamente.
// Teste unitário para classe de erro de domínio
function testInsufficientFundsError() {
const error = new InsufficientFundsError("acc123", 100, 200);
assert(error.code === "INSUFFICIENT_FUNDS");
assert(error.type === "DOMAIN_ERROR");
assert(error.details.deficit === 100);
assert(error.details.currentBalance === 100);
assert(error.details.requestedAmount === 200);
console.log("✓ Teste InsufficientFundsError passou");
}
// Teste de integração no middleware
async function testMiddlewareMapping() {
const req = mockRequest({ path: "/transfer", correlationId: "abc123" });
const res = mockResponse();
// Teste erro de domínio
const domainError = new InvalidCpfError("123.456.789-00", "digito verificador inválido");
await errorHandlerMiddleware(domainError, req, res, () => {});
assert(res.statusCode === 400);
assert(res.body.error.code === "INVALID_CPF");
// Teste erro de infraestrutura
const infraError = new DatabaseConnectionError(new Error("ECONNREFUSED"), "query");
await errorHandlerMiddleware(infraError, req, res, () => {});
assert(res.statusCode === 503);
assert(res.body.error.message.includes("temporariamente indisponível"));
console.log("✓ Teste de middleware passou");
}
// Simulação de falha de infraestrutura
async function testDatabaseOffline() {
const mockDb = { query: () => { throw new Error("ECONNREFUSED"); } };
try {
await executeQueryWithInfrastructureError(mockDb, "SELECT * FROM users");
} catch (error) {
assert(error instanceof DatabaseConnectionError);
assert(error.code === "DATABASE_CONNECTION_FAILED");
assert(error.statusCode === 503);
}
console.log("✓ Teste de falha de banco passou");
}
8. Boas práticas finais e armadilhas comuns
Boas práticas:
- Sempre use classes específicas para cada tipo de erro de domínio
- Mantenha um arquivo central com todos os códigos de erro documentados
- Inclua campo helpUrl em erros de domínio para direcionar desenvolvedores à documentação
- Use correlation IDs para rastrear erros através de múltiplos serviços
- Versionar explicitamente o envelope de erro na documentação da API
Armadilhas a evitar:
- Nunca vazar detalhes de infraestrutura em respostas de erro (ex: nomes de tabelas, endereços de servidores)
- Nunca usar throw new Error("deu ruim") para erros de domínio — isso perde toda a semântica
- Evitar códigos de erro genéricos como "VALIDATION_ERROR" — prefira códigos específicos
- Não misturar responsabilidades: erros de domínio não devem conter detalhes técnicos de implementação
- Não ignorar a necessidade de testes para cenários de erro — eles são tão importantes quanto os fluxos felizes
Manter consistência entre times é fundamental: adote um padrão único de classes, códigos de erro e envelopes de resposta. Documente esse padrão em um guia interno e revise-o periodicamente com a equipe.
Referências
-
MDN Web Docs: HTTP response status codes — Documentação oficial sobre códigos de status HTTP, essencial para mapear corretamente erros de domínio (4xx) e infraestrutura (5xx).
-
Microsoft REST API Guidelines: Error Handling — Diretrizes da Microsoft para tratamento de erros em APIs REST, incluindo envelopes padronizados e códigos de erro.
-
Google Cloud API Design Guide: Errors — Guia do Google para design de erros em APIs, com exemplos de modelagem de mensagens de erro e boas práticas.
-
Stripe API Reference: Errors — Documentação oficial da Stripe sobre estrutura de erros, referência em como modelar erros de domínio com códigos específicos e mensagens amigáveis.
-
Zalando RESTful API Guidelines: Error Handling — Diretrizes da Zalando para tratamento de erros em APIs RESTful, com foco em separação entre erros de negócio e técnicos.