Definición
HATEOAS (Hypermedia as the Engine of Application State, Hipermedia como Motor del Estado de la Aplicación) es la restricción más avanzada y a menudo ignorada de la arquitectura REST. Significa que los clientes interactúan con una API REST completamente a través de enlaces de hipermedia proporcionados dinámicamente por el servidor. En lugar de codificar URLs o seguir una secuencia fija de llamadas a la API desde la documentación, los clientes descubren acciones disponibles examinando enlaces en las respuestas.
Piensa en HATEOAS como navegar la web. No comienzas con una lista de cada URL en un sitio web - aterrizas en la página de inicio y haces clic en enlaces para navegar. Si una página tiene un botón “Comprar Ahora”, puedes hacer clic en él. Si no, esa acción no está disponible. HATEOAS trae esta misma descubribilidad a las APIs: el servidor te dice qué puedes hacer a continuación basándose en el estado actual.
Esta es la “gloria de REST” (Nivel 3 en el Modelo de Madurez de Richardson) y la restricción menos implementada. La mayoría de las APIs requieren que los clientes lean documentación y codifiquen patrones de URL, violando HATEOAS. Una API verdaderamente RESTful permite a los clientes navegar puramente siguiendo enlaces, haciendo que la API sea auto-documentada y permitiendo a los servidores cambiar estructuras de URL sin romper clientes.
Ejemplo
Flujo de Pull Request de GitHub: Cuando recuperas un pull request vía GET /repos/:owner/:repo/pulls/:number, GitHub devuelve los datos del PR más enlaces de hipermedia basados en el estado. Si el PR está abierto y es mergeable, la respuesta incluye "mergeable": true y un enlace "merge_url". Si ya fue mergeado, ese enlace desaparece, y aparece un "commits_url" en su lugar. El cliente no necesita conocer el flujo de trabajo del PR - sigue los enlaces proporcionados.
Flujo de Aprobación de Pago de PayPal: Cuando creas un pago vía PayPal, la respuesta incluye "approval_url": "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-XXXX". Rediriges al usuario allí (siguiendo el enlace), PayPal maneja la aprobación, luego devuelve al usuario con un nuevo enlace para ejecutar el pago: "execute": "/v1/payments/payment/PAY-123/execute". Cada transición de estado es guiada por hipermedia - no se necesitan URLs codificadas.
Sesión de Checkout de Stripe: La API de Checkout de Stripe crea una sesión con POST /v1/checkout/sessions. La respuesta incluye "url": "https://checkout.stripe.com/pay/cs_test_XXXX" - rediriges al usuario a esta URL (enlace de hipermedia). Después del pago, Stripe redirige de vuelta con las URLs de éxito o cancelación que proporcionaste. El cliente sigue enlaces, el servidor controla el flujo.
Ciclo de Vida del Pedido: Imagina una API de e-commerce donde los pedidos transicionan a través de estados: pendiente → pagado → enviado → entregado. Un pedido pendiente incluye enlaces: "pay": "/orders/123/payment", "cancel": "/orders/123". Una vez pagado, el enlace pay desaparece, cancel puede permanecer, y aparece un nuevo enlace "track_shipment": "/orders/123/tracking". Las acciones disponibles cambian con el estado, todo comunicado vía hipermedia.
Recomendación de Producto de Amazon: Al ver un producto, las APIs internas de Amazon probablemente devuelven enlaces de productos relacionados: "frequently_bought_together": ["/products/456", "/products/789"], "customers_also_viewed": ["/products/111", "/products/222"]. El cliente no calcula estos - sigue los enlaces proporcionados por el servidor para navegar el catálogo de productos.
Analogía
El Tour Guiado del Museo: Entras a un museo y obtienes una audioguía. En cada exhibición, la guía te dice qué puedes hacer a continuación: “Gira a la izquierda para Arte Renacentista, gira a la derecha para Escultura Moderna, o sube al Café de la Azotea.” No necesitas un mapa - la guía (hipermedia) te dirige basándose en tu ubicación actual (estado). Si el Café de la Azotea está cerrado, esa opción no se menciona. HATEOAS funciona de la misma manera - el servidor te guía a través de acciones disponibles.
La Interfaz del Cajero Automático: Cuando insertas tu tarjeta, el cajero muestra opciones disponibles: Retirar, Depositar, Consultar Saldo. Si tu cuenta está sobregira, “Retirar” podría no aparecer. Después de seleccionar “Retirar,” el cajero te guía a la siguiente pantalla (ingresar monto), luego la siguiente (tomar efectivo), luego ofrece nuevas opciones (¿imprimir recibo?). Navegas siguiendo indicaciones (enlaces), no memorizando secuencias de botones.
Navegación GPS Paso a Paso: El GPS no te da la ruta completa por adelantado. Te dice el siguiente giro, luego el siguiente, adaptándose si tomas un giro equivocado. Cada instrucción se basa en tu estado actual (ubicación). HATEOAS proporciona la misma guía dinámica basada en estado - cada respuesta incluye los siguientes pasos válidos.
El Videojuego de Aventura: En juegos como The Legend of Zelda, no puedes acceder a áreas hasta que hayas completado prerequisitos. El juego te muestra rutas disponibles (enlaces de hipermedia) basadas en tu progreso (estado). Si no tienes el gancho, los bordes del acantilado no son escalables. A medida que ganas ítems, se abren nuevos caminos. HATEOAS hace que las APIs se comporten así - las acciones disponibles emergen a medida que cambia el estado.
Ejemplo de Código
// Ejemplo: Flujo de Trabajo de Pedido con HATEOAS
// Cada estado proporciona diferentes enlaces de hipermedia
// ===== ESTADO 1: Pedido Creado (Pago Pendiente) =====
// GET /api/orders/789
{
"id": 789,
"user_id": 123,
"status": "pending",
"total": 59.98,
"created_at": "2026-01-09T10:30:00Z",
// HATEOAS: Acciones disponibles basadas en el estado actual
"_links": {
"self": {
"href": "/api/orders/789",
"method": "GET",
"rel": "self"
},
"user": {
"href": "/api/users/123",
"method": "GET",
"rel": "related"
},
"items": {
"href": "/api/orders/789/items",
"method": "GET",
"rel": "collection"
},
// Acción: Pagar el pedido
"pay": {
"href": "/api/orders/789/payment",
"method": "POST",
"rel": "action"
},
// Acción: Actualizar detalles del pedido
"update": {
"href": "/api/orders/789",
"method": "PATCH",
"rel": "edit"
},
// Acción: Cancelar el pedido
"cancel": {
"href": "/api/orders/789",
"method": "DELETE",
"rel": "delete"
}
}
}
// ===== TRANSICIÓN DE ESTADO: Cliente sigue enlace "pay" =====
// POST /api/orders/789/payment
// { "payment_method": "card", "token": "tok_visa" }
// El servidor procesa el pago y transiciona el pedido al estado "paid"
// ===== ESTADO 2: Pedido Pagado =====
// GET /api/orders/789
{
"id": 789,
"user_id": 123,
"status": "paid",
"total": 59.98,
"created_at": "2026-01-09T10:30:00Z",
"paid_at": "2026-01-09T10:35:00Z",
// HATEOAS: Diferentes enlaces - ¡el estado cambió!
"_links": {
"self": {
"href": "/api/orders/789",
"method": "GET",
"rel": "self"
},
"user": {
"href": "/api/users/123",
"method": "GET",
"rel": "related"
},
"items": {
"href": "/api/orders/789/items",
"method": "GET",
"rel": "collection"
},
// Enlace "pay" eliminado (ya pagado)
// Enlace "update" eliminado (no se puede modificar pedido pagado)
// Enlace "cancel" eliminado (no se puede cancelar pedido pagado)
// Nuevos enlaces basados en nuevo estado:
"invoice": {
"href": "/api/orders/789/invoice.pdf",
"method": "GET",
"rel": "related"
},
"track_shipment": {
"href": "/api/orders/789/tracking",
"method": "GET",
"rel": "related"
},
"request_refund": {
"href": "/api/orders/789/refund",
"method": "POST",
"rel": "action"
}
}
}
// ===== ESTADO 3: Pedido Enviado =====
// Después de que el almacén envía el pedido
// GET /api/orders/789
{
"id": 789,
"user_id": 123,
"status": "shipped",
"total": 59.98,
"created_at": "2026-01-09T10:30:00Z",
"paid_at": "2026-01-09T10:35:00Z",
"shipped_at": "2026-01-09T14:20:00Z",
"tracking_number": "1Z999AA10123456784",
"_links": {
"self": {
"href": "/api/orders/789",
"method": "GET",
"rel": "self"
},
"user": {
"href": "/api/users/123",
"method": "GET",
"rel": "related"
},
"items": {
"href": "/api/orders/789/items",
"method": "GET",
"rel": "collection"
},
"invoice": {
"href": "/api/orders/789/invoice.pdf",
"method": "GET",
"rel": "related"
},
// Seguimiento mejorado ahora disponible
"tracking": {
"href": "/api/orders/789/tracking",
"method": "GET",
"rel": "related"
},
"tracking_external": {
"href": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"method": "GET",
"rel": "external"
},
// Reembolso todavía posible pero más restringido
"request_refund": {
"href": "/api/orders/789/refund",
"method": "POST",
"rel": "action",
"note": "Reembolso disponible dentro de 30 días después de la entrega"
}
}
}
// ===== CÓDIGO CLIENTE: Navegando vía HATEOAS =====
// El cliente no codifica URLs - sigue enlaces
async function navigateOrder(orderId) {
// Comenzar recuperando el pedido
let response = await fetch(`/api/orders/${orderId}`);
let order = await response.json();
// El cliente examina enlaces disponibles
if (order._links.pay) {
// El enlace "pay" existe, así que el pago es posible
console.log("El pedido está pendiente de pago");
console.log("Pagar en:", order._links.pay.href);
}
if (order._links.track_shipment) {
// El enlace "track_shipment" existe, así que el pedido fue enviado o está en tránsito
console.log("El pedido fue enviado");
console.log("Rastrear en:", order._links.track_shipment.href);
}
// El cliente sigue enlaces dinámicamente
if (order._links.invoice) {
// Descargar factura siguiendo el enlace
const invoiceUrl = order._links.invoice.href;
window.open(invoiceUrl, '_blank');
}
// Sin URLs codificadas - toda la navegación vía hipermedia
}
Diagrama
stateDiagram-v2
[*] --> Created: POST /api/orders
state Created {
[*] --> Links1
Links1: _links.pay
Links1: _links.update
Links1: _links.cancel
}
Created --> Paid: Seguir enlace pay
POST /api/orders/789/payment
state Paid {
[*] --> Links2
Links2: _links.invoice
Links2: _links.track_shipment
Links2: _links.request_refund
Links2: ❌ pay (eliminado)
Links2: ❌ cancel (eliminado)
}
Paid --> Shipped: Almacén envía
Servidor actualiza estado
state Shipped {
[*] --> Links3
Links3: _links.tracking
Links3: _links.tracking_external
Links3: _links.invoice
Links3: _links.request_refund*
}
Shipped --> Delivered: Entrega confirmada
state Delivered {
[*] --> Links4
Links4: _links.invoice
Links4: _links.leave_review
Links4: _links.reorder
Links4: ❌ request_refund (30 días pasados)
}
Delivered --> [*]
Created --> Cancelled: Seguir enlace cancel
DELETE /api/orders/789
Cancelled --> [*]
note right of Created
El cliente descubre acciones
examinando _links
end note
note right of Paid
Las acciones disponibles cambian
basándose en el estado
end note
note right of Shipped
La hipermedia guía
al cliente a través del flujo
end note
Notas de Seguridad
Requisitos Principales:
- HATEOAS puede exponer inadvertidamente información sensible de estado a través de enlaces disponibles.
- Implementar autorización apropiada antes de incluir enlaces de acción - no revelar enlaces para acciones que el usuario no puede realizar.
- Validar permisos del lado del servidor incluso si un cliente descubrió un enlace, ya que los clientes pueden construir URLs manualmente.
- Usar enlaces firmados o tokenizados para operaciones sensibles para prevenir acceso no autorizado vía manipulación de URL.
- Incluir tokens CSRF en enlaces que cambien estado cuando se usen cookies para autenticación.
- No exponer IDs de recursos internos o lógica de negocio a través de estructuras de enlaces.
Mejores Prácticas:
- Limitar la tasa de descubrimiento de enlaces para prevenir ataques de enumeración.
- Validar que los clientes solo puedan acceder a enlaces para recursos que poseen.
- Usar HTTPS exclusivamente para proteger enlaces de hipermedia en tránsito.
- Implementar expiración de enlaces para acciones sensibles al tiempo (ej.
- enlaces de aprobación de pago).
- Registrar patrones sospechosos de travesía de enlaces que puedan indicar reconocimiento..
Mejores Prácticas
- Incluir enlaces para todas las transiciones de estado válidas: Solo mostrar acciones que el recurso puede realizar actualmente
- Usar tipos de relación de enlaces estándar:
self,related,next,prev,edit,deletede RFC 5988 - Eliminar acciones no disponibles: No mostrar enlace “cancel” si el pedido ya fue enviado
- Incluir información del método: Especificar GET, POST, PUT, DELETE para cada enlace
- Usar URLs absolutas: Evitar rutas relativas que requieran construcción del lado del cliente
- Seguir estándares de hipermedia: Usar HAL, JSON:API, Siren o Collection+JSON
- Hacer enlaces auto-documentados: Usar atributos
reldescriptivos y campostitleopcionales - Implementar plantillas de enlaces: Usar plantillas URI de RFC 6570 para enlaces parametrizados
- Proporcionar formularios para acciones complejas: Incluir información de esquema para payloads POST/PUT
- Comenzar simple, evolucionar: Comenzar con HATEOAS básico y agregar complejidad según sea necesario
Errores Comunes
Incluir todos los enlaces posibles independientemente del estado: Mostrar enlace “pay” incluso cuando el pedido ya fue pagado viola los principios de HATEOAS.
Codificar URLs en el código del cliente: Derrota el propósito de HATEOAS y crea acoplamiento fuerte.
Faltan atributos rel: Los clientes no pueden entender el propósito del enlace sin tipos de relación.
Formatos de enlace inconsistentes: Mezclar diferentes estándares de hipermedia confunde a los clientes.
Sin documentación de relaciones de enlaces: Valores rel personalizados sin documentación son inútiles.
Ignorar autorización: Mostrar enlaces para acciones que el usuario no está permitido realizar es un riesgo de seguridad.
URLs relativas sin base: Proporcionar /orders/123 en lugar de https://api.example.com/orders/123 requiere que los clientes conozcan la URL base.
Documentación estática en lugar de descubrimiento dinámico: Escribir “para pagar un pedido, POST a /orders/:id/payment” en docs en lugar de proporcionar el enlace dinámicamente.
Romper contratos de enlaces: Cambiar estructuras de enlaces entre versiones de API rompe clientes que siguen enlaces.
Sobre-ingeniería: Implementar HATEOAS complejo para APIs simples y estables donde agrega poco valor.