O Problem Details no Node.js padroniza respostas de erro HTTP usando o media type application/problem+json. Em vez de cada endpoint inventar campos como error, message, code e reason, a API usa uma estrutura comum com type, title, status, detail e instance.
O formato é definido pela RFC 9457, que substituiu a RFC 7807. Ele não muda o status HTTP nem transforma detalhes internos em informações públicas. O objetivo é oferecer uma representação previsível para clientes humanos e máquinas, permitindo extensões específicas como erros de validação, saldo insuficiente ou conflito de versão.
Neste guia, você aprenderá a implementar Problem Details em Node.js, Express e Fastify, criar tipos estáveis, tratar validação, esconder dados sensíveis, documentar com OpenAPI e testar o contrato.
O que é RFC 9457?
A RFC 9457 define um objeto JSON para representar detalhes de problemas em APIs HTTP. O formato usa:
Content-Type: application/problem+jsonExemplo:
{
"type": "https://api.exemplo.com/problems/insufficient-credit",
"title": "Crédito insuficiente",
"status": 403,
"detail": "O saldo atual não permite concluir a compra.",
"instance": "/problems/01JXYZ",
"balance": 30,
"required": 50
}Campos padrão
- type: URI que identifica o tipo do problema;
- title: resumo curto e estável;
- status: status HTTP, usado como conveniência;
- detail: explicação específica da ocorrência;
- instance: identificador da ocorrência.
Clientes devem usar type como identificador principal, não analisar o texto de detail.
O status real continua importante
O campo status não substitui a linha HTTP:
HTTP/1.1 422 Unprocessable ContentOs dois valores precisam ser iguais. Proxies, caches e bibliotecas genéricas usam o status HTTP real.
Tipo about:blank
Quando type está ausente, o valor implícito é about:blank. Nesse caso, o problema não possui semântica adicional além do status.
{
"type": "about:blank",
"title": "Not Found",
"status": 404
}Para erros de domínio que os clientes precisam tratar, crie uma URI própria e estável.
Classe ProblemDetails
export class ProblemDetails extends Error {
constructor({
type = 'about:blank',
title,
status = 500,
detail,
instance,
extensions = {},
cause
}) {
super(detail ?? title, { cause });
this.name = 'ProblemDetails';
this.type = type;
this.title = title;
this.status = status;
this.detail = detail;
this.instance = instance;
this.extensions = extensions;
}
toJSON() {
return {
type: this.type,
title: this.title,
status: this.status,
...(this.detail ? { detail: this.detail } : {}),
...(this.instance ? { instance: this.instance } : {}),
...this.extensions
};
}
}Veja Tratamento de Erros no Node.js para classes, cause e logging.
Factory de problemas
export const problems = {
notFound(resource, id) {
return new ProblemDetails({
type: 'https://api.exemplo.com/problems/not-found',
title: 'Recurso não encontrado',
status: 404,
detail: `${resource} não foi encontrado.`,
extensions: { resource, id }
});
},
conflict(detail, version) {
return new ProblemDetails({
type: 'https://api.exemplo.com/problems/conflict',
title: 'Conflito de atualização',
status: 409,
detail,
extensions: { currentVersion: version }
});
}
};Não exponha IDs ou valores que o usuário não tem autorização para conhecer.
Middleware no Express
app.use((error, req, res, next) => {
if (res.headersSent) return next(error);
const problem = error instanceof ProblemDetails
? error
: new ProblemDetails({
type: 'https://api.exemplo.com/problems/internal-error',
title: 'Erro interno',
status: 500,
detail: 'Não foi possível concluir a operação.'
});
const instance = problem.instance ??
`/problems/${crypto.randomUUID()}`;
logger.error({
error,
problemType: problem.type,
instance,
requestId: req.id
}, 'Falha na requisição');
res
.status(problem.status)
.type('application/problem+json')
.send({ ...problem.toJSON(), instance });
});O stack permanece apenas nos logs internos.
Error handler no Fastify
fastify.setErrorHandler((error, request, reply) => {
const problem = error instanceof ProblemDetails
? error
: new ProblemDetails({
type: 'https://api.exemplo.com/problems/internal-error',
title: 'Erro interno',
status: 500,
detail: 'Não foi possível concluir a operação.'
});
request.log.error({ error }, 'request_failed');
reply
.code(problem.status)
.type('application/problem+json')
.send(problem.toJSON());
});Consulte Fastify com Node.js para hooks e schemas.
Erros de validação
A RFC permite extensões. Um formato útil:
{
"type": "https://api.exemplo.com/problems/validation-error",
"title": "Requisição inválida",
"status": 422,
"errors": [
{
"pointer": "#/email",
"detail": "deve ser um e-mail válido"
},
{
"pointer": "#/items/0/quantity",
"detail": "deve ser maior que zero"
}
]
}Use JSON Pointer para indicar o campo. Não coloque regras internas ou regex sensíveis.
400 ou 422?
Use 400 quando a requisição não pode ser interpretada ou possui sintaxe inválida. Use 422 quando o conteúdo é compreendido, mas viola regras de validação ou domínio. Mantenha a política consistente.
401 e 403
- 401: autenticação ausente ou inválida;
- 403: identidade conhecida sem permissão.
Não revele se um recurso privado existe quando isso cria enumeração.
404
Um problema 404 pode incluir apenas uma descrição genérica. Evite retornar consultas SQL, caminhos internos ou detalhes sobre soft delete.
409 Conflict
Use para conflito de versão, estado ou recurso duplicado:
{
"type": "https://api.exemplo.com/problems/version-conflict",
"title": "Versão desatualizada",
"status": 409,
"detail": "O recurso foi alterado por outra operação.",
"currentVersion": 8
}Veja Lock Otimista no Node.js.
429 Too Many Requests
Inclua Retry-After quando apropriado:
reply.header('Retry-After', '60');{
"type": "https://api.exemplo.com/problems/rate-limit",
"title": "Muitas requisições",
"status": 429,
"detail": "Tente novamente após o intervalo indicado."
}Consulte Rate Limiting em APIs Node.js.
503 Service Unavailable
Use quando a dependência ou serviço está temporariamente indisponível. Não converta todo erro desconhecido em 503; isso pode incentivar retries perigosos.
Type URI
Prefira URI absoluta:
https://api.exemplo.com/problems/payment-declinedA página pode documentar:
- significado;
- status recomendado;
- extensões;
- ações do cliente;
- exemplos;
- histórico de mudanças.
Alterar a URI cria um novo tipo e pode ser breaking change.
Instance
instance identifica uma ocorrência:
/problems/01JABCDEFO identificador pode ser correlacionado com logs, mas não deve permitir acesso público a detalhes sem autenticação.
Request ID e trace ID
Uma extensão pode oferecer correlação:
{
"instance": "/problems/01JABCDEF",
"traceId": "8d2f..."
}Avalie se expor trace ID é aceitável. O cliente não precisa receber dados sobre topologia interna.
Internacionalização
title e detail podem ser localizados conforme Accept-Language. O cliente deve continuar usando type, status e extensões para lógica.
Negociação de conteúdo
Responda com application/problem+json. Mesmo quando o cliente aceita JSON genérico, o media type específico comunica o contrato.
OpenAPI
components:
schemas:
ProblemDetails:
type: object
properties:
type:
type: string
format: uri-reference
title:
type: string
status:
type: integer
minimum: 100
maximum: 599
detail:
type: string
instance:
type: string
format: uri-reference
required: [type, title, status]
responses:
ValidationProblem:
description: Requisição inválida
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'Veja OpenAPI com Node.js.
Clientes tipados
O cliente deve tratar tipos conhecidos e ter fallback:
switch (problem.type) {
case 'https://api.exemplo.com/problems/validation-error':
showValidation(problem.errors);
break;
case 'https://api.exemplo.com/problems/rate-limit':
scheduleRetry(response.headers.get('retry-after'));
break;
default:
showGenericError(problem.title);
}Ignore extensões desconhecidas para permitir evolução compatível.
Logging
Registre:
- type;
- status;
- instance;
- request ID;
- trace ID;
- erro original;
- rota normalizada;
- versão do serviço.
Não copie automaticamente o corpo da requisição.
Segurança
- Não retorne stack.
- Não retorne SQL.
- Não exponha caminhos.
- Não revele tokens.
- Não detalhe dependências internas.
- Redija dados pessoais.
- Use mensagens genéricas em 500.
Testes
test('retorna Problem Details na validação', async () => {
const response = await app.inject({
method: 'POST',
url: '/orders',
payload: { items: [] }
});
assert.equal(response.statusCode, 422);
assert.match(
response.headers['content-type'],
/application\/problem\+json/
);
const body = response.json();
assert.equal(body.status, 422);
assert.equal(
body.type,
'https://api.exemplo.com/problems/validation-error'
);
assert.ok(Array.isArray(body.errors));
});Consulte Node Test Runner.
Testes de contrato
Valide que:
- status real e campo status coincidem;
- type é estável;
- content-type está correto;
- campos obrigatórios existem;
- stack nunca aparece;
- extensões possuem tipos corretos;
- clientes ignoram campos novos.
Erros comuns
- Usar detail como código: texto pode mudar.
- Type aleatório: clientes não conseguem mapear.
- Status divergente: intermediários interpretam errado.
- Stack no corpo: informação sensível é exposta.
- 200 com erro: semântica HTTP é quebrada.
- Extensões sem documentação: contrato fica implícito.
- Um tipo para tudo: clientes perdem precisão.
- Tipos demais: manutenção vira complexa.
Conclusão
O Problem Details no Node.js oferece um contrato consistente para erros HTTP. A RFC 9457 define campos estáveis, extensões e um media type específico sem substituir os status existentes.
Use type URIs documentadas, esconda internals, mantenha status coerentes e teste o contrato. Com OpenAPI e logging correlacionado, clientes tratam falhas de forma previsível e a equipe investiga ocorrências sem criar um formato novo para cada endpoint.


