Mejores prácticas para el manejo de errores en API REST
Las respuestas de error de tu API forman parte de su contrato. Los clientes las analizan, la lógica de reintento depende de ellas y los equipos de soporte las consultan a las 2 a.m. Sin embargo, muchos equipos diseñan la ruta feliz en detalle y dejan que los errores dependan de los valores predeterminados del framework. El resultado: tres formatos de error distintos, respuestas 200 que contienen "success": false y rastreos de pila que exponen el esquema de la base de datos públicamente.
Esta guía explica cómo diseñar un contrato de errores coherente para servicios REST: elegir el código de estado correcto, estandarizar el cuerpo con RFC 9457 Problem Details, separar códigos legibles por máquina de mensajes humanos, marcar errores reintentables y mantener los secretos fuera de las respuestas. También muestra cómo probar cada ruta de falla en Apidog, porque un contrato de errores que nunca pruebas es un contrato que no tienes.
Empieza con el código de estado
HTTP ya proporciona una primera capa de semántica. RFC 9110 define estas familias:
- 4xx: el cliente hizo algo incorrecto; repetir la misma solicitud volverá a fallar.
- 5xx: el servidor falló; la solicitud del cliente pudo ser válida.
Define esta división antes de diseñar el cuerpo de error. Clientes genéricos, proxies, cachés y bibliotecas de reintento toman decisiones basándose en el código sin leer tu JSON.
Consulta la referencia de códigos de estado HTTP de MDN y usa esta tabla para resolver los casos más habituales:
| Situación | Usar | No usar | Por qué |
|---|---|---|---|
| Solicitud mal formada: JSON roto, tipo de contenido incorrecto o campo requerido faltante | 400 Bad Request |
422 |
El servidor no puede analizar o entender la solicitud |
| Solicitud válida sintácticamente, pero con reglas semánticas incumplidas: cantidad negativa o moneda no soportada | 422 Unprocessable Content |
400 |
La sintaxis es correcta; los valores no |
| Sin credenciales o con token caducado/inválido | 401 Unauthorized |
403 |
El cliente no ha demostrado su identidad; envía WWW-Authenticate
|
| Credenciales válidas, pero permisos insuficientes | 403 Forbidden |
401 |
La identidad se conoce y el acceso está denegado |
| El recurso nunca existió o no confirmarás que existe | 404 Not Found |
410 |
Es el valor predeterminado seguro y puede ocultar recursos ante sondeos no autorizados |
| El recurso existió y fue eliminado permanentemente | 410 Gone |
404 |
Indica a clientes y rastreadores que eliminen sus referencias |
| Conflicto de estado: clave duplicada, versión obsoleta o colisión de edición | 409 Conflict |
400 |
La solicitud es válida, pero choca con el estado actual |
| El cliente excedió un límite de tasa | 429 Too Many Requests |
503 |
Incluye siempre Retry-After
|
| Excepción no manejada en tu código | 500 Internal Server Error |
502 |
Tu servidor falló |
| El servicio ascendente devolvió una respuesta inválida a tu gateway | 502 Bad Gateway |
500 |
El fallo está en la dependencia, no en el gateway |
| El servidor está sobrecargado o en mantenimiento | 503 Service Unavailable |
500 |
Es temporal por definición; añade Retry-After cuando sea posible |
| El servicio ascendente agotó el tiempo de espera | 504 Gateway Timeout |
500 |
Distingue una dependencia lenta de un error en tu código |
Dos casos requieren especial atención:
-
401 frente a 403: es un límite de seguridad, no una preferencia de estilo. Devolver
403a un cliente no autenticado puede revelar que el recurso existe. -
429 sin
Retry-After: enseña a los clientes a reintentar en bucles cerrados. Si aplicas límites de tasa, combina el estado con una instrucción concreta de retroceso. Consulta nuestra guía sobre limitación de tasa de API.
Usa un único cuerpo de error: RFC 9457 Problem Details
Una vez elegido el código correcto, todos los errores deberían compartir el mismo tipo de medio y esquema: application/problem+json.
RFC 9457 define cinco miembros principales:
-
type: URI que identifica la categoría del error. -
title: resumen humano breve. -
status: código HTTP, repetido por conveniencia. -
detail: descripción de lo ocurrido en esta instancia. -
instance: URI del fallo específico.
El resto debe expresarse mediante miembros de extensión definidos por tu contrato. Consulta el explicador de RFC 9457 para conocer la especificación completa y su relación con RFC 7807.
Este es un error de validación para un endpoint de pagos:
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/v1/payments/requests/req_9f3c1a7b",
"code": "PAYMENT_VALIDATION_FAILED",
"errors": [
{
"field": "amount",
"code": "AMOUNT_NOT_POSITIVE",
"message": "amount must be a positive integer in minor units"
}
],
"request_id": "req_9f3c1a7b"
}
errors[] es una extensión especialmente útil: permite que un frontend asocie cada fallo con el campo exacto del formulario. Mantén las rutas de campo estables. Elige un formato, como JSON Pointer o rutas con puntos, y úsalo en toda la API.
Aplica este formato también a los errores generados por tu framework o gateway. Si tus manejadores devuelven Problem Details, pero el balanceador responde con HTML para un 502, los clientes tendrán que mantener dos analizadores.
Separa códigos de mensajes
El ejemplo contiene code y message porque sirven a audiencias distintas:
- Códigos legibles por máquina: forman parte del contrato. Deben ser estables, documentados y enumerables.
- Mensajes humanos: pueden cambiar, mejorar y traducirse sin romper clientes.
Códigos como AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED e IDEMPOTENCY_KEY_REUSED permiten que los clientes tomen decisiones programáticas. Nunca hagas que analicen prosa:
if (message.includes("positive")) {
// La edición de texto acaba de convertirse en un cambio incompatible
}
Un mensaje como "la cantidad debe ser un entero positivo en unidades menores" es más útil que "cantidad inválida". Si localizas los mensajes, deja los códigos sin cambios.
Esta separación es aún más importante cuando los consumidores incluyen agentes autónomos. Los clientes basados en LLM se recuperan mejor de errores estructurados y autoexplicativos; consulta el artículo sobre diseño de errores de API para agentes de IA.
Qué nunca debe aparecer en una respuesta
Las respuestas de error son una fuente frecuente de reconocimiento para atacantes. El middleware debe impedir que llegue al cliente cualquiera de estos elementos:
- Rastros de pila, nombres de clases o rutas de archivos.
- SQL en bruto, fragmentos de consultas o errores del ORM.
- Nombres de host internos, direcciones IP, puertos o nombres de servicios.
- Versiones de bibliotecas y banners del framework.
- Secretos, tokens o cadenas de conexión incluidos en excepciones.
- Información sobre si una cuenta existe, especialmente en login y recuperación de contraseñas.
Captura las excepciones en el límite de la aplicación, registra el detalle completo en el servidor junto con un ID de solicitud y devuelve un Problem Details genérico con ese mismo ID:
{
"detail": "An internal error occurred",
"request_id": "req_51ad0"
}
El cliente recibe información segura, los registros conservan la causa real y soporte puede relacionar ambos mediante request_id.
Marca los errores reintentables
Cada error responde a una pregunta del cliente: ¿debo intentarlo de nuevo? Incluye esa semántica en el contrato para que cada SDK no tenga que adivinar.
Como regla general:
-
429,502,503y504: reintentables con retroceso exponencial y jitter. -
500: puede justificar un reintento cauteloso. - La mayoría de los demás
4xx: terminales. Reintentar401,403,404o422con la misma solicitud desperdicia cuota.
Los tiempos de espera requieren cuidado adicional: la operación pudo completarse después de que el cliente abandonara. En endpoints de mutación, usa claves de idempotencia para evitar cargos o creaciones duplicadas.
También puedes indicar la decisión explícitamente:
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
La propiedad retryable permite anular la semántica predeterminada. Por ejemplo, un 500 concreto podría ser terminal si reintentarlo corrompe el estado. Documenta la regla una vez para que todos tus SDK implementen el mismo comportamiento.
Añade IDs de correlación y versiona el contrato
Dos decisiones pequeñas son baratas ahora y costosas después.
Usa un ID por solicitud
Acepta un encabezado X-Request-Id entrante o genera uno. Inclúyelo en cada línea de registro y en cada cuerpo de error como request_id. En sistemas distribuidos, propaga también traceparent de W3C.
Cuando un cliente incluya un error en un ticket, ese ID convertirá una búsqueda de registros de una hora en una sola consulta.
Evoluciona el contrato de forma compatible
Añadir un nuevo miembro de extensión o un nuevo código de error suele ser seguro. En cambio, renombrar errors[].field, cambiar el significado de un código o sustituir un formato propio por Problem Details rompe clientes.
Las URI de type ofrecen un mecanismo limpio:
- Mantén estables las URI antiguas.
- Introduce nuevas URI para semánticas nuevas.
- Documenta que los miembros y códigos desconocidos deben ignorarse.
Esta regla de compatibilidad hacia adelante permite evolucionar sin crear una v2 para cada cambio.
Prueba cada ruta de error en Apidog
Los contratos de error se degradan cuando nadie los ejecuta. La ruta feliz aparece en cada demo; la rama 422 solo aparece cuando un cliente la activa. Convierte los fallos en casos de prueba de primera clase e integra las pruebas en CI.
Pruebas del lado del servidor
Crea un escenario por cada caso de fallo:
- Falta de autenticación:
401. - Rol insuficiente:
403. - Cantidad negativa:
422yerrors[0].code == AMOUNT_NOT_POSITIVE. - Tráfico en ráfaga:
429con encabezadoRetry-After.
Las aserciones de API de Apidog permiten verificar códigos de estado, encabezados y campos del cuerpo. También puedes validar toda la respuesta contra el esquema JSON de Problem Details para que cualquier desviación falle en CI y no en producción.
Servidores simulados del lado del cliente
Los equipos de frontend y SDK necesitan trabajar con respuestas 4xx y 5xx antes de que el backend pueda producirlas bajo demanda.
Los servidores simulados de Apidog devuelven los cuerpos exactos de Problem Details definidos en tu especificación. Así puedes simular:
- Un
503conRetry-After: 120. - Un
409por envío duplicado. - Una carga completa de validación con
errors[].
Después, verifica cómo el cliente renderiza el error y decide si reintentar. No necesitas crear un stub de Express ni modificar temporalmente el backend.
Diseña el contrato, conviértelo en escenarios y simulaciones e integra ambos en CI. Puedes descargar Apidog e importar una especificación OpenAPI existente para obtener respuestas de error simulables en pocos minutos.
Preguntas frecuentes
¿Debo usar 400 o 422 para errores de validación?
Usa 400 cuando la solicitud está mal formada y el servidor no puede entenderla: JSON inválido, tipo de contenido incorrecto o campo requerido faltante.
Usa 422 cuando la solicitud se analiza correctamente, pero sus valores incumplen reglas de dominio, como una cantidad negativa o una moneda no compatible.
La distinción ayuda al diagnóstico: 422 significa “corrige tus datos”; 400, “corrige el formato de la solicitud”. Lo importante es aplicar la misma regla en todos los endpoints.
¿Qué es application/problem+json?
Es el tipo de medio definido por RFC 9457 para Problem Details, el formato estándar de errores JSON para API HTTP. Incluye type, title, status, detail e instance, además de extensiones como errors[].
El tipo de medio registrado permite que clientes genéricos y middleware reconozcan los errores sin configuración personalizada. Consulta el explicador de RFC 9457.
¿Qué errores HTTP deben reintentar automáticamente los clientes?
Reintenta 429, 502, 503 y 504 con retroceso exponencial y jitter, respetando Retry-After cuando exista. Trata 500 como potencialmente reintentable, pero con cautela.
No reintentes otras respuestas 4xx: la solicitud fallará de la misma forma. En endpoints que modifican datos, combina los reintentos con claves de idempotencia.
¿Cómo pruebo las respuestas de error sin romper mi backend?
Simúlalas. Apunta el cliente a un servidor simulado de Apidog que devuelva los cuerpos 4xx y 5xx definidos en tu especificación. Después verifica la renderización y el comportamiento de reintento.
En el servidor, crea escenarios con cargas inválidas, autenticación faltante y tráfico en ráfaga. Comprueba códigos de estado, encabezados y esquema del cuerpo. Ejecuta ambas partes en CI para mantener el contrato honesto sin forzar manualmente los fallos.
Top comments (0)