Manejo consistente de errores y validación en APIs REST

Capítulo 8

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Por qué estandarizar errores y validaciones

Un manejo consistente de errores reduce ambigüedad para clientes (web, móvil, integraciones) y simplifica observabilidad en el servidor. La meta es que cualquier respuesta de error sea: predecible (misma forma), accionable (indica qué corregir), trazable (permite correlación), y segura (no filtra detalles internos).

Formato estándar de error (contrato)

Define un único “sobre” (envelope) para errores, independientemente del endpoint. Un formato práctico incluye: código interno estable, mensaje legible, detalles por campo, traceId/correlationId y enlaces a documentación.

Esquema recomendado

{  "error": {    "code": "USR_EMAIL_INVALID",    "message": "El email no tiene un formato válido.",    "status": 422,    "traceId": "01HZY...",    "details": [      {        "field": "email",        "issue": "format",        "message": "Debe ser un email válido.",        "value": "foo@"      }    ],    "links": {      "about": "https://api.ejemplo.com/docs/errors#USR_EMAIL_INVALID"    }  }}
  • error.code: código interno estable (no depende del idioma ni del texto).
  • error.message: texto legible para humanos (puede ser localizado).
  • error.status: redundante pero útil para clientes y logs.
  • error.traceId: correlación para soporte y debugging (idealmente también en header).
  • error.details: lista de problemas (por campo o por regla).
  • error.links.about: URL a documentación del error (sin palabras en la imagen, pero aquí sí aplica).

Headers complementarios

  • X-Request-Id o Traceparent (W3C): correlación distribuida.
  • Content-Type: application/problem+json (opcional): si adoptas RFC 7807, mantén consistencia y mapea tus campos cuidadosamente.

Diferenciar errores de cliente vs servidor

La regla operativa: si el cliente puede corregir la solicitud, es un error de cliente; si el cliente no puede hacer nada razonable, es servidor o infraestructura. Aunque el status HTTP ya lo sugiere, tu error.code debe reforzarlo.

CategoríaEjemplosSeñalesAcción del cliente
Cliente (4xx)Validación, autenticación, autorización, conflicto, rate limitSolicitud inválida o no permitidaCorregir datos, credenciales, permisos o reintentar más tarde
Servidor (5xx)Errores no controlados, dependencias caídasFallo interno o upstreamReintentar con backoff; reportar con traceId

Validaciones coherentes: 400 vs 422 y conflictos 409

Cuándo usar 400 (Bad Request)

Úsalo cuando la solicitud es sintácticamente inválida o no puede parsearse/interpretarse: JSON mal formado, tipos imposibles, parámetros incompatibles a nivel de formato.

HTTP/1.1 400 Bad Request{  "error": {    "code": "REQ_MALFORMED_JSON",    "message": "El cuerpo JSON no es válido.",    "status": 400,    "traceId": "..."  }}

Cuándo usar 422 (Unprocessable Entity)

Úsalo cuando la solicitud es sintácticamente correcta, pero falla reglas de negocio o validaciones semánticas: campos requeridos, rangos, formatos, combinaciones inválidas.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

HTTP/1.1 422 Unprocessable Entity{  "error": {    "code": "REQ_VALIDATION_FAILED",    "message": "Hay errores de validación.",    "status": 422,    "traceId": "...",    "details": [      { "field": "password", "issue": "minLength", "message": "Mínimo 12 caracteres." },      { "field": "birthDate", "issue": "range", "message": "Debe ser una fecha pasada." }    ],    "links": { "about": "https://api.ejemplo.com/docs/errors#REQ_VALIDATION_FAILED" }  }}

Cuándo usar 409 (Conflict)

Úsalo cuando la solicitud es válida, pero entra en conflicto con el estado actual del recurso o una restricción de unicidad/consistencia: email ya registrado, versión desactualizada, transición de estado no permitida.

HTTP/1.1 409 Conflict{  "error": {    "code": "USR_EMAIL_ALREADY_EXISTS",    "message": "Ya existe un usuario con ese email.",    "status": 409,    "traceId": "...",    "details": [      { "field": "email", "issue": "unique", "message": "El email ya está en uso." }    ]  }}

Cómo representar múltiples errores

Evita responder “uno por uno” obligando al cliente a iterar. Devuelve una lista en error.details con todos los problemas detectados en una pasada.

Convenciones útiles para details

  • field: ruta del campo (por ejemplo address.street o items[3].sku).
  • issue: clave estable de la regla (required, format, min, max, enum).
  • message: texto legible (localizable).
  • value: opcional; cuidado con PII (ver sección de seguridad).
  • location: opcional para distinguir body, query, path, header.
{  "error": {    "code": "REQ_VALIDATION_FAILED",    "message": "Hay errores de validación.",    "status": 422,    "traceId": "...",    "details": [      { "location": "query", "field": "limit", "issue": "max", "message": "Máximo 100." },      { "location": "body", "field": "items[0].quantity", "issue": "min", "message": "Debe ser al menos 1." }    ]  }}

Errores de autenticación y autorización

401 Unauthorized (no autenticado o token inválido)

Úsalo cuando faltan credenciales o son inválidas/expiradas. Incluye WWW-Authenticate cuando aplique (por ejemplo, Bearer).

HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer error="invalid_token"{  "error": {    "code": "AUTH_INVALID_TOKEN",    "message": "La sesión no es válida o expiró.",    "status": 401,    "traceId": "...",    "links": { "about": "https://api.ejemplo.com/docs/errors#AUTH_INVALID_TOKEN" }  }}

403 Forbidden (autenticado pero sin permisos)

Úsalo cuando el usuario está autenticado pero no tiene autorización. Evita revelar si el recurso existe cuando eso sea sensible; en algunos casos se prefiere responder 404 para no enumerar recursos.

HTTP/1.1 403 Forbidden{  "error": {    "code": "AUTH_FORBIDDEN",    "message": "No tienes permisos para realizar esta acción.",    "status": 403,    "traceId": "..."  }}

Rate limiting: 429 Too Many Requests

Cuando el cliente excede límites, responde 429 e incluye headers estándar para guiar reintentos. Mantén el cuerpo con tu formato de error.

  • Retry-After: segundos o fecha HTTP.
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (si usas este esquema).
HTTP/1.1 429 Too Many RequestsRetry-After: 30X-RateLimit-Limit: 60X-RateLimit-Remaining: 0X-RateLimit-Reset: 1700000000{  "error": {    "code": "RATE_LIMIT_EXCEEDED",    "message": "Has excedido el límite de solicitudes. Intenta más tarde.",    "status": 429,    "traceId": "...",    "details": [      { "issue": "retryAfter", "message": "Reintenta en 30 segundos." }    ],    "links": { "about": "https://api.ejemplo.com/docs/errors#RATE_LIMIT_EXCEEDED" }  }}

Guía práctica paso a paso para implementar consistencia

Paso 1: Define un catálogo de errores con códigos estables

Crea un catálogo versionado (en repositorio) con códigos, status, descripción, y si es “exponible” al cliente. Evita códigos generados dinámicamente.

CódigoStatusUsoExponible
REQ_MALFORMED_JSON400JSON inválido
REQ_VALIDATION_FAILED422Validación semántica
USR_EMAIL_ALREADY_EXISTS409Unicidad
AUTH_INVALID_TOKEN401Token inválido
AUTH_FORBIDDEN403Sin permisos
RATE_LIMIT_EXCEEDED429Límite excedido
INTERNAL_ERROR500Fallo no controladoSí (genérico)

Convención recomendada: prefijos por dominio o capa (REQ_, AUTH_, USR_, PAY_, etc.). Mantén los códigos estables aunque cambie el texto.

Paso 2: Centraliza el mapeo de excepciones a respuestas

Implementa un manejador global (middleware/filtro) que convierta errores internos a tu formato estándar. Esto evita que cada controlador “invente” su respuesta.

  • Errores de parseo → REQ_MALFORMED_JSON (400).
  • Errores de validación → REQ_VALIDATION_FAILED (422) con details.
  • Violación de unicidad/estado → códigos de conflicto (409).
  • Errores no previstos → INTERNAL_ERROR (500) con mensaje genérico.

Paso 3: Diseña validaciones por capas (y coherentes)

  • Validación de entrada (forma): tipos, requeridos, longitudes, formatos.
  • Validación de negocio (reglas): invariantes, transiciones de estado, límites por plan.
  • Validación de consistencia (concurrencia): versiones, conflictos, recursos ya existentes.

Regla práctica: si el error se puede atribuir a un campo o regla concreta, debe aparecer en details.

Paso 4: Estandariza traceId y correlación

Genera un traceId por request (o adopta el de tu gateway) y devuélvelo en:

  • Header: X-Request-Id: ...
  • Cuerpo: error.traceId

En logs, registra siempre traceId, error.code, status, endpoint y actor (si aplica).

Paso 5: Documenta enlaces por error

En la documentación, cada error.code debe tener: significado, causas comunes, cómo resolver, ejemplos de respuesta y campos en details. El link links.about debe ser estable.

Pautas para no filtrar información sensible

Qué no devolver al cliente

  • Stack traces, nombres de tablas/columnas, consultas SQL, rutas de archivos, configuración interna.
  • Mensajes de excepciones de dependencias (DB, colas, servicios internos) sin sanitizar.
  • Datos personales en details.value (tokens, contraseñas, documentos, emails completos si no es necesario).

Patrones seguros

  • Mensaje externo genérico, detalle interno en logs: el cliente recibe INTERNAL_ERROR, el servidor registra la excepción completa con traceId.
  • Minimización de datos: en validación, reporta el campo y la regla; evita eco de valores sensibles. Ejemplo: para password, nunca devuelvas value.
  • Evitar enumeración: en autenticación/recuperación de cuenta, no confirmes si un usuario existe (mismo mensaje para “email no existe” y “email existe”).
  • Consistencia en 404/403: cuando la existencia del recurso sea sensible, considera responder 404 en lugar de 403.

Ejemplos integrales por escenario

Parámetro de consulta inválido (400)

GET /users?limit=abcHTTP/1.1 400 Bad Request{  "error": {    "code": "REQ_INVALID_QUERY_PARAM",    "message": "Parámetro de consulta inválido.",    "status": 400,    "traceId": "...",    "details": [      { "location": "query", "field": "limit", "issue": "type", "message": "Debe ser un número." }    ]  }}

Validación de payload (422) con múltiples errores

POST /users{ "email": "foo@", "password": "123" }HTTP/1.1 422 Unprocessable Entity{  "error": {    "code": "REQ_VALIDATION_FAILED",    "message": "Hay errores de validación.",    "status": 422,    "traceId": "...",    "details": [      { "field": "email", "issue": "format", "message": "Debe ser un email válido." },      { "field": "password", "issue": "minLength", "message": "Mínimo 12 caracteres." }    ]  }}

Conflicto por unicidad (409)

POST /users{ "email": "ana@ejemplo.com", "password": "..." }HTTP/1.1 409 Conflict{  "error": {    "code": "USR_EMAIL_ALREADY_EXISTS",    "message": "Ya existe un usuario con ese email.",    "status": 409,    "traceId": "...",    "details": [      { "field": "email", "issue": "unique", "message": "El email ya está en uso." }    ]  }}

Acceso sin token (401) vs sin permisos (403)

GET /admin/reportsHTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer{  "error": {    "code": "AUTH_MISSING_TOKEN",    "message": "Faltan credenciales de autenticación.",    "status": 401,    "traceId": "..."  }}
GET /admin/reportsHTTP/1.1 403 Forbidden{  "error": {    "code": "AUTH_FORBIDDEN",    "message": "No tienes permisos para realizar esta acción.",    "status": 403,    "traceId": "..."  }}

Rate limit (429) con reintento guiado

HTTP/1.1 429 Too Many RequestsRetry-After: 30{  "error": {    "code": "RATE_LIMIT_EXCEEDED",    "message": "Has excedido el límite de solicitudes. Intenta más tarde.",    "status": 429,    "traceId": "..."  }}

Ahora responde el ejercicio sobre el contenido:

¿En qué situación es más apropiado responder con HTTP 422 y devolver una lista de problemas en error.details?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

El 422 se usa cuando la solicitud se entiende y es sintácticamente válida, pero no cumple validaciones semánticas o reglas de negocio. Para evitar respuestas “uno por uno”, se reportan todos los problemas en error.details.

Siguiente capítulo

Versionado y compatibilidad hacia atrás en APIs REST

Arrow Right Icon
Portada de libro electrónico gratuitaDiseño de APIs REST: buenas prácticas, errores comunes y estándares
80%

Diseño de APIs REST: buenas prácticas, errores comunes y estándares

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.