¿Cómo debería una API devolver errores? ¿Cómo es una buena respuesta de error estandarizada (por ejemplo, RFC 7807 Problem Details)?
Una respuesta de error sigue siendo parte del contrato de tu API — 'se rompió, aquí tienes un stack trace' no es un diseño, y RFC 7807 le da a cualquier cliente la misma forma para interpretar.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo debería una API devolver errores? ¿Cómo es una buena respuesta de error estandarizada (por ejemplo, RFC 7807 Problem Details)?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué los cuerpos de error improvisados duelen
Todo backend que ha trabajado en un equipo grande conoce alguna API donde los errores fueron una idea de último momento: un endpoint devuelve {"error": "not found"}, otro devuelve {"message": "Invalid input", "err": true}, un tercero responde con un 200 OK con forma de éxito que envuelve por dentro {"success": false}, y un cuarto, cuando algo se rompe de verdad, deja que el stack trace de una excepción sin manejar se filtre directo al cuerpo de la respuesta. Un cliente que integra contra esa API no puede escribir un solo camino de manejo de errores — tiene que hacer un caso especial por cada endpoint, y no tiene ningún campo estable sobre el cual ramificar su lógica.
Una buena respuesta de error tiene que satisfacer a varias audiencias a la vez: necesita un código de estado HTTP correcto para que la infraestructura genérica (reintentos, circuit breakers, monitoreo) se comporte con sensatez sin siquiera leer el cuerpo; necesita un identificador estable y parseable por máquina para que el código del cliente pueda ramificar según qué tipo de error es, no solo mostrar un código de estado; necesita un mensaje legible para humanos, útil en logs, tickets de soporte y desarrolladores depurando una integración; y nunca debe filtrar detalles internos —nada de stack traces, SQL crudo ni rutas de archivos internas— en una respuesta de producción.
#RFC 7807: Problem Details para APIs HTTP
RFC 7807 (pronto renumerado como RFC 9457, con la misma forma) estandariza un cuerpo JSON exactamente para esto, servido con el tipo de medio application/problem+json para que un cliente pueda distinguir un error estructurado de un cuerpo JSON de éxito normal con solo mirar el Content-Type. La forma tiene cinco miembros reservados, y eres libre de agregar los tuyos:
type— una URI que identifica la categoría del problema. No tiene que resolver a algo real, pero debería ser estable y, idealmente, apuntar a documentación legible para humanos sobre esa clase de error. Por defecto esabout:blanksi se omite, lo cual solo significa "mira el código de estado".title— un resumen corto y legible para humanos del tipo de problema, pensado para ser el mismo en cada ocurrencia de ese tipo (no pongas aquí detalle específico de la petición).status— el código de estado HTTP, duplicado en el cuerpo puramente por conveniencia (pipelines de logging y clientes que inspeccionan el cuerpo sin también acarrear el código de estado por separado).detail— una explicación legible para humanos específica de esta ocurrencia, por ejemplo qué campo o qué recurso.instance— una URI que identifica esta ocurrencia en particular, normalmente solo la ruta de la petición, útil para correlacionar con los logs.
Cualquier cosa más allá de esos cinco es un "miembro de extensión" — simplemente claves JSON adicionales al lado de las estándar, y aquí es donde pones lo que tu error específico realmente necesita.
{"type": "https://api.example.com/errors/insufficient-funds","title": "Insufficient Funds","status": 422,"detail": "La cuenta 4f3a-99c1 tiene un saldo de 12.50, menor a la transferencia solicitada de 50.00.","instance": "/transfers/7a4e1c3d","balance": 12.50,"requested": 50.00}
Los errores de validación son el caso más común que necesita un miembro de extensión, ya que una sola petición puede fallar por varias razones a la vez:
{"type": "https://api.example.com/errors/validation-failed","title": "Validation Failed","status": 400,"detail": "Uno o más campos fallaron la validación.","instance": "/users","errors": [{ "field": "email", "message": "debe ser una dirección de correo válida" },{ "field": "age", "message": "debe ser un entero positivo" }]}
#Conectándolo
Una sola clase de error más un middleware centralizado de manejo de errores suele bastar para que esto sea consistente en toda la superficie de una API, sin que cada handler de ruta individual tenga que recordar la forma del envelope:
class ApiError extends Error {constructor({ status, title, detail, type, ...extensions }) {super(detail || title);this.status = status;this.title = title;this.detail = detail;this.type = type || 'about:blank';this.extensions = extensions;}}function errorHandler(err, req, res, next) {const status = err.status || 500;res.status(status).type('application/problem+json').json({type: err.type,title: err.title || 'Internal Server Error',status,detail: status >= 500 ? undefined : err.detail, // nunca filtres detalles internos en 5xxinstance: req.originalUrl,...err.extensions,});}module.exports = { ApiError, errorHandler };
#Elegir el código de estado correcto
El formato del cuerpo solo ayuda si el código de estado que lo acompaña es correcto — un cliente que solo tiene tiempo de revisar una cosa va a revisar el código de estado primero.
| Estado | Cuándo usarlo |
|---|---|
400 Bad Request | Sintaxis malformada que el servidor ni siquiera puede parsear (JSON roto, content type equivocado) |
401 Unauthorized | Credenciales faltantes o inválidas — la petición ni siquiera está autenticada |
403 Forbidden | Autenticado, pero sin permiso para hacer esto |
404 Not Found | El recurso no existe (o estás ocultando intencionalmente que existe) |
409 Conflict | La petición entra en conflicto con el estado actual (recurso duplicado, desajuste de versión) |
422 Unprocessable Entity | Sintácticamente válida pero semánticamente inválida (viola una regla de negocio) |
429 Too Many Requests | Límite de tasa alcanzado — acompáñalo con una cabecera Retry-After |
500 Internal Server Error | Algo se rompió que el cliente no pudo haber evitado |
503 Service Unavailable | Caído o sobrecargado temporalmente — acompáñalo con Retry-After |
🧠 Check your understanding
0/3 · 0/3 answered1. En RFC 7807 Problem Details, ¿para qué sirve el campo 'type'?
2. ¿Por qué una respuesta de fallo de validación debería incluir un campo 'status' dentro del cuerpo JSON, si la línea de estado HTTP ya lo lleva?
3. ¿Cuál es el principal peligro de poner mensajes de excepción crudos en el campo detail de una respuesta de error en un 500 de producción?