Códigos de estado HTTP y contratos de respuesta consistentes

Capítulo 4

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Por qué importan los códigos de estado y un contrato de respuesta

En una API, el código de estado HTTP comunica el resultado a nivel de protocolo; el cuerpo y las cabeceras completan el “contrato” que el cliente usa para reaccionar (reintentar, cachear, mostrar errores, seguir enlaces, etc.). Un contrato consistente reduce lógica condicional en clientes, facilita observabilidad y evita ambigüedades (por ejemplo, “200 con un error dentro del JSON”).

Principios de consistencia

  • El código de estado debe reflejar el resultado real: éxito, redirección/caché, error del cliente o del servidor.
  • Formato de error uniforme para todos los 4xx/5xx (misma estructura, mismos campos base).
  • Cabeceras coherentes: Content-Type siempre que haya cuerpo; Location al crear; ETag para concurrencia/caché; Retry-After cuando aplique.
  • No inventar semántica: evitar “200 OK” con {"success": false}.

Mapa práctico: escenarios comunes → código → cuerpo/cabeceras

Respuestas de éxito (2xx)

EscenarioCódigoCuerpoCabeceras clave
Lectura exitosa (GET)200 OKRepresentación del recurso o colecciónContent-Type, opcional ETag, Cache-Control
Creación sincrónica (POST crea recurso)201 CreatedRecomendado: representación del recurso creadoLocation (URL del nuevo recurso), Content-Type, opcional ETag
Procesamiento asíncrono aceptado202 AcceptedOpcional: estado/recibo con enlace a recurso de operaciónLocation (URL de estado), opcional Retry-After
Éxito sin contenido (DELETE o PUT sin representación)204 No ContentSin cuerpoEvitar Content-Type; puede incluir ETag si aplica a la respuesta (raro)

Regla clave: si respondes 204, no envíes cuerpo (ni siquiera {}). Si necesitas devolver datos (por ejemplo, el recurso actualizado), usa 200.

Caché y validación (304)

EscenarioCódigoCuerpoCabeceras clave
GET condicional: el recurso no cambió304 Not ModifiedSin cuerpoETag y/o Last-Modified (si se usa), Cache-Control

Para habilitar 304, el servidor debe emitir un validador (típicamente ETag) en respuestas 200. El cliente envía If-None-Match y el servidor responde 304 si no hay cambios.

Errores del cliente (4xx)

EscenarioCódigoCuándo usarloCuerpo recomendadoCabeceras clave
Solicitud mal formada400 Bad RequestJSON inválido, parámetros imposibles, tipos incorrectosError estándar + detalles de parsing/paramContent-Type
No autenticado401 UnauthorizedFalta token/credenciales o son inválidasError estándarWWW-Authenticate (si aplica), Content-Type
Autenticado pero sin permisos403 ForbiddenEl usuario no puede acceder/operarError estándarContent-Type
No existe el recurso404 Not FoundID inexistente o ruta no encontradaError estándarContent-Type
Método no permitido405 Method Not AllowedRuta existe pero verbo no soportadoError estándarAllow (lista de métodos), Content-Type
Conflicto de estado409 ConflictDuplicados, violación de unicidad, conflicto de concurrencia (según convención)Error estándar + causaContent-Type, opcional ETag
Recurso ya no disponible410 GoneExistió pero fue retirado permanentementeError estándarContent-Type
Media type no soportado415 Unsupported Media TypeContent-Type enviado no soportadoError estándar + lista soportadaAccept (opcional), Content-Type
Entidad válida sintácticamente pero inválida semánticamente422 Unprocessable EntityValidación de negocio/campos (formato correcto, reglas fallan)Error estándar + errores por campoContent-Type
Demasiadas solicitudes429 Too Many RequestsRate limit excedidoError estándar + info de límiteRetry-After, opcional X-RateLimit-*

400 vs 422: usa 400 cuando la solicitud no se puede interpretar correctamente (sintaxis/estructura). Usa 422 cuando se entiende, pero no pasa validaciones (por ejemplo, email con formato válido pero ya registrado, o startDate > endDate).

Errores del servidor y upstream (5xx)

EscenarioCódigoCuándo usarloCuerpo recomendadoCabeceras clave
Error inesperado500 Internal Server ErrorExcepción no controlada, bugError estándar (sin filtrar detalles sensibles)Content-Type, opcional Retry-After si es transitorio
Fallo en dependencia/upstream502 Bad GatewayGateway/proxy recibe respuesta inválida de upstreamError estándar + referenciaContent-Type
Servicio temporalmente no disponible503 Service UnavailableMantenimiento, saturación, degradaciónError estándarRetry-After recomendado, Content-Type

Convenciones recomendadas para el cuerpo

Contrato de éxito

Define una convención estable por tipo de respuesta:

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

  • Recurso individual: el JSON del recurso.
  • Colección: {"items": [...], "page": {...}} (si hay paginación) o un array directo si tu estándar lo permite, pero sé consistente.
  • 202 (asíncrono): un “operation resource” con estado.

Contrato de error uniforme (4xx/5xx)

Usa un objeto de error consistente. Ejemplo de estructura mínima:

{  "error": {    "code": "validation_failed",    "message": "One or more fields are invalid.",    "details": [      {"field": "email", "issue": "invalid_format"},      {"field": "age", "issue": "must_be_greater_or_equal", "min": 18}    ],    "requestId": "c0a8012e-7b1d-4f2a-9b1a-2b2c3d4e5f6a"  }}
  • code: identificador estable para lógica del cliente (no dependas solo de message).
  • message: legible para humanos.
  • details: lista opcional para validación/diagnóstico.
  • requestId: correlación con logs/tracing (también puede ir en cabecera).

Cabeceras: convenciones y uso correcto

Content-Type

  • En respuestas con cuerpo JSON: Content-Type: application/json; charset=utf-8.
  • En 204 y 304: normalmente no enviar Content-Type porque no hay cuerpo.

Accept

  • El cliente envía Accept para indicar formatos aceptados.
  • Si no puedes responder con un formato aceptable, lo correcto es 406 Not Acceptable (no está en la lista requerida, pero es útil conocerlo). Si tu API no negocia contenido, documenta que solo se soporta JSON y valida Accept de forma pragmática.

Location

  • En 201 Created: URL del recurso creado.
  • En 202 Accepted: URL donde consultar el estado de la operación (o el recurso resultante si ya existe pero está procesándose).

ETag

  • Servidor devuelve ETag en 200 (y a menudo en 201) para identificar versión.
  • Cliente puede usar If-None-Match (caché) y If-Match (concurrencia optimista). Si usas If-Match y falla, el código típico es 412 Precondition Failed (no incluido en la lista, pero relevante si adoptas ETags para escritura).

Cache-Control

  • Define cacheabilidad explícita: Cache-Control: private, max-age=60 o no-store para datos sensibles.
  • Combina con ETag para GET condicional eficiente.

Retry-After

  • En 429 y 503 (y a veces 202): indica cuándo reintentar.
  • Puede ser segundos o fecha HTTP: Retry-After: 120.

Tablas de decisión (cheat sheets)

Decisión para operaciones de escritura (POST/PUT/PATCH/DELETE)

PreguntaSiNo
¿La solicitud es sintácticamente inválida (JSON roto, tipos imposibles)?400 + errorSiguiente
¿Falta autenticación o es inválida?401 + error (+ WWW-Authenticate)Siguiente
¿Está autenticado pero no autorizado?403 + errorSiguiente
¿El recurso objetivo no existe?404 + errorSiguiente
¿El método no está permitido en esa ruta?405 + error (+ Allow)Siguiente
¿El Content-Type no es soportado?415 + errorSiguiente
¿Falla validación semántica/reglas de negocio?422 + error con detailsSiguiente
¿Hay conflicto (duplicado/estado incompatible)?409 + errorSiguiente
¿Se crea un recurso nuevo sincrónicamente?201 + Location (+ representación)Siguiente
¿Se procesa asíncronamente?202 + Location (+ estado) (+ Retry-After)Siguiente
¿La operación tuvo éxito pero no hay contenido que devolver?204 sin cuerpo200 + representación

Decisión para GET con caché

CondiciónRespuestaNotas
Recurso existe y el cliente no envía validadores200 + cuerpoIncluye ETag y Cache-Control si aplica
Cliente envía If-None-Match y no cambió304 sin cuerpoMantén cabeceras de caché/validación
Cliente envía If-None-Match y cambió200 + cuerpoNuevo ETag

Ejemplos de respuestas completas

200 OK (GET) con ETag y caché

HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8Cache-Control: private, max-age=60ETag: "v3-9f2c1a"{  "id": "u_123",  "name": "Ada Lovelace",  "email": "ada@example.com"}

304 Not Modified (GET condicional)

HTTP/1.1 304 Not ModifiedCache-Control: private, max-age=60ETag: "v3-9f2c1a"

201 Created (POST) con Location y representación

HTTP/1.1 201 CreatedContent-Type: application/json; charset=utf-8Location: https://api.example.com/users/u_124ETag: "v1-1a2b3c"{  "id": "u_124",  "name": "Grace Hopper",  "email": "grace@example.com"}

202 Accepted (proceso asíncrono) con Location y Retry-After

HTTP/1.1 202 AcceptedContent-Type: application/json; charset=utf-8Location: https://api.example.com/operations/op_789Retry-After: 10{  "operationId": "op_789",  "status": "pending",  "submittedAt": "2026-02-03T10:15:30Z"}

204 No Content (DELETE)

HTTP/1.1 204 No Content

400 Bad Request (JSON inválido)

HTTP/1.1 400 Bad RequestContent-Type: application/json; charset=utf-8{  "error": {    "code": "invalid_json",    "message": "Request body is not valid JSON.",    "requestId": "b7b2a1d0-2c3d-4e5f-9a10-11c12d13e14f"  }}

401 Unauthorized (sin credenciales)

HTTP/1.1 401 UnauthorizedContent-Type: application/json; charset=utf-8WWW-Authenticate: Bearer realm="api", error="invalid_token"{  "error": {    "code": "unauthenticated",    "message": "Authentication is required.",    "requestId": "2f1c0d9e-8b7a-6c5d-4e3f-2a1b0c9d8e7f"  }}

403 Forbidden (sin permisos)

HTTP/1.1 403 ForbiddenContent-Type: application/json; charset=utf-8{  "error": {    "code": "forbidden",    "message": "You do not have permission to perform this action.",    "requestId": "f1e2d3c4-b5a6-7980-1234-56789abcdeff"  }}

404 Not Found

HTTP/1.1 404 Not FoundContent-Type: application/json; charset=utf-8{  "error": {    "code": "not_found",    "message": "User 'u_999' was not found.",    "requestId": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9"  }}

405 Method Not Allowed (con Allow)

HTTP/1.1 405 Method Not AllowedContent-Type: application/json; charset=utf-8Allow: GET, POST{  "error": {    "code": "method_not_allowed",    "message": "Method PUT is not allowed for this endpoint.",    "requestId": "aa11bb22-cc33-dd44-ee55-ff6677889900"  }}

409 Conflict (duplicado)

HTTP/1.1 409 ConflictContent-Type: application/json; charset=utf-8{  "error": {    "code": "conflict",    "message": "Email is already in use.",    "details": [      {"field": "email", "issue": "already_exists"}    ],    "requestId": "13579bdf-2468-ace0-1357-9bdf2468ace0"  }}

410 Gone (recurso retirado)

HTTP/1.1 410 GoneContent-Type: application/json; charset=utf-8{  "error": {    "code": "gone",    "message": "This resource has been permanently removed.",    "requestId": "01234567-89ab-cdef-0123-456789abcdef"  }}

415 Unsupported Media Type

HTTP/1.1 415 Unsupported Media TypeContent-Type: application/json; charset=utf-8{  "error": {    "code": "unsupported_media_type",    "message": "Content-Type 'text/plain' is not supported. Use 'application/json'.",    "requestId": "deafbeef-dead-beef-dead-beefdeadbeef"  }}

422 Unprocessable Entity (validación)

HTTP/1.1 422 Unprocessable EntityContent-Type: application/json; charset=utf-8{  "error": {    "code": "validation_failed",    "message": "One or more fields are invalid.",    "details": [      {"field": "email", "issue": "invalid_format"},      {"field": "password", "issue": "too_short", "minLength": 12}    ],    "requestId": "11111111-2222-3333-4444-555555555555"  }}

429 Too Many Requests (rate limit) con Retry-After

HTTP/1.1 429 Too Many RequestsContent-Type: application/json; charset=utf-8Retry-After: 60{  "error": {    "code": "rate_limited",    "message": "Too many requests. Please retry later.",    "details": [      {"limit": 100, "windowSeconds": 60}    ],    "requestId": "99999999-8888-7777-6666-555555555555"  }}

500 Internal Server Error

HTTP/1.1 500 Internal Server ErrorContent-Type: application/json; charset=utf-8{  "error": {    "code": "internal_error",    "message": "An unexpected error occurred.",    "requestId": "abcdabcd-abcd-abcd-abcd-abcdabcdabcd"  }}

502 Bad Gateway

HTTP/1.1 502 Bad GatewayContent-Type: application/json; charset=utf-8{  "error": {    "code": "bad_gateway",    "message": "Upstream service returned an invalid response.",    "requestId": "123e4567-e89b-12d3-a456-426614174000"  }}

503 Service Unavailable con Retry-After

HTTP/1.1 503 Service UnavailableContent-Type: application/json; charset=utf-8Retry-After: 120{  "error": {    "code": "service_unavailable",    "message": "Service is temporarily unavailable. Please retry later.",    "requestId": "fedcba98-7654-3210-fedc-ba9876543210"  }}

Guía paso a paso para definir tu contrato de respuestas

Paso 1: Define el “perfil” de respuesta por categoría

  • Éxito con cuerpo: 200/201/202 → JSON + Content-Type.
  • Éxito sin cuerpo: 204 y 304 → sin cuerpo.
  • Error: 4xx/5xx → JSON de error uniforme + Content-Type.

Paso 2: Establece el objeto de error estándar

Documenta campos obligatorios (por ejemplo error.code, error.message, requestId) y opcionales (details). Asegura que todos los endpoints lo devuelvan igual.

Paso 3: Mapea escenarios a códigos (tabla de decisión)

Adopta una tabla como las anteriores y úsala como checklist en revisiones de API. Esto evita inconsistencias entre equipos.

Paso 4: Define cabeceras obligatorias por respuesta

  • 201: Location obligatorio.
  • 405: Allow obligatorio.
  • 429/503: Retry-After recomendado.
  • Respuestas cacheables: Cache-Control y ETag recomendados.

Paso 5: Valida automáticamente

Implementa tests contractuales que verifiquen: (1) código correcto por escenario, (2) ausencia de cuerpo en 204/304, (3) presencia de cabeceras requeridas, (4) forma del error uniforme en 4xx/5xx.

Ahora responde el ejercicio sobre el contenido:

En una operación de escritura, el servidor entiende el JSON y la estructura es válida, pero la solicitud falla por reglas de negocio (por ejemplo, validación semántica). ¿Qué código de estado es el más adecuado?

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

¡Tú error! Inténtalo de nuevo.

Usa 422 cuando la solicitud es sintácticamente correcta y se puede procesar, pero falla por validaciones semánticas o reglas de negocio. 400 es para errores de sintaxis/estructura, y 409 para conflictos de estado como duplicados o concurrencia.

Siguiente capítulo

Paginación y límites de resultados en colecciones REST

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

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.