En la lección anterior vimos que REST se cumple en grados, y que casi ninguna API real es plenamente RESTful. Eso deja una pregunta abierta muy práctica: ¿cómo se mide ese grado? Leonard Richardson propuso en 2008 un modelo de cuatro niveles que se ha convertido en el vocabulario estándar del sector. En esta lección recorreremos los cuatro niveles reescribiendo la misma operación de Tienda Aroma —crear un pedido— en cada uno, de modo que la evolución se vea con los ojos. Después entraremos en HATEOAS, el nivel más alto y más discutido, veremos formatos hipermedia reales como HAL y JSON:API, y cerraremos con un debate honesto sobre cuándo compensa llegar hasta arriba y cuándo no.
Contenido
- Para qué sirve el modelo de madurez
- Nivel 0: el pantano de POX
- Nivel 1: recursos
- Nivel 2: verbos HTTP y códigos de estado
- Nivel 3: controles hipermedia (HATEOAS)
- Los cuatro niveles de un vistazo
- Qué es HATEOAS y qué problema resuelve
- Formatos hipermedia: HAL, JSON:API y Siren
- El debate honesto: ¿por qué casi nadie llega al nivel 3?
- Criterios para decidir tu nivel
- Para qué sirve el modelo de madurez
Leonard Richardson presentó este modelo en la conferencia QCon de 2008, y Martin Fowler lo popularizó en un artículo de 2010. Su virtud es doble:
- Ofrece un vocabulario compartido: decir "estamos en nivel 2 y no pensamos subir" comunica en cinco palabras una decisión de arquitectura completa.
- Convierte una discusión binaria y estéril ("¿esto es REST o no?") en una escala progresiva sobre la que se puede razonar.
Dos advertencias antes de empezar:
- No es de Fielding ni es normativo. Es un modelo descriptivo, útil para diagnosticar, no un examen que haya que aprobar.
- Subir de nivel no es automáticamente mejor. Cada nivel tiene un coste. El objetivo es elegir con criterio, no maximizar la puntuación.
- Nivel 0: el pantano de POX
POX significa Plain Old XML (XML del montón), aunque hoy el pantano es más bien de JSON. Sus señas de identidad:
- Un único endpoint para todo.
- Un único método, casi siempre
POST. - La operación que se quiere ejecutar va dentro del cuerpo.
- HTTP se usa como simple túnel de transporte: sus métodos, sus códigos y su caché se ignoran por completo.
Así crearía Tienda Aroma un pedido en nivel 0:
POST /api HTTP/1.1
Host: api.tiendaaroma.example
Content-Type: application/json
{
"accion": "crearPedido",
"parametros": {
"clienteId": "cli_842",
"lineas": [{ "cafeId": "caf_001", "cantidad": 2 }]
}
}Y la respuesta:
HTTP/1.1 200 OK
Content-Type: application/json
{
"exito": true,
"resultado": { "pedidoId": "ped_5001", "totalEuros": 29.00 }
}Y así consultaría ese mismo pedido:
POST /api HTTP/1.1
Content-Type: application/json
{ "accion": "obtenerPedido", "parametros": { "pedidoId": "ped_5001" } }Qué está mal, en términos concretos y medibles:
| Problema | Consecuencia práctica |
|---|---|
Todo va por POST |
Ninguna respuesta se puede cachear, ni por el navegador ni por una CDN |
| No hay URIs de recursos | No se puede compartir ni marcar un enlace a un pedido |
El error viaja en el cuerpo con 200 OK |
La monitorización no detecta fallos; los reintentos automáticos no funcionan |
La semántica está en accion |
Ningún intermediario entiende nada sin conocer tu dominio |
| No hay idempotencia | Un reintento tras un fallo de red crea un segundo pedido |
Esto es, esencialmente, RPC sobre HTTP. Es exactamente lo que hacía SOAP (lección 01-06) y lo que hace hoy... GraphQL, que también usa un único endpoint y POST (lección 01-07). Con una diferencia importante: GraphQL lo hace de forma deliberada, con un contrato tipado y herramientas propias que compensan lo que pierde. El nivel 0 suele ser accidental.
- Nivel 1: recursos
El primer salto: dejar de tener un único endpoint y dar identidad propia a cada cosa del dominio. Ya no se habla con "la API", se habla con recursos concretos.
POST /v1/pedidos HTTP/1.1
Content-Type: application/json
{
"accion": "crear",
"clienteId": "cli_842",
"lineas": [{ "cafeId": "caf_001", "cantidad": 2 }]
}HTTP/1.1 200 OK
Content-Type: application/json
{ "exito": true, "pedidoId": "ped_5001", "totalEuros": 29.00 }Consultar ese pedido, en nivel 1:
Qué se ha ganado:
- Cada pedido tiene URI propia:
/v1/pedidos/ped_5001. Se puede enlazar, registrar en logs y enrutar de forma diferenciada. - Se puede repartir la carga por recurso: los pedidos a unos servidores, el catálogo a otros.
- La API es mucho más comprensible al leer un log o una traza.
Qué falta todavía:
- Se sigue usando
POSTpara todo, incluso para leer. Sin caché, y sin distinguir lectura de escritura. - El verbo sigue en el cuerpo (
accion). - Los códigos de estado siguen sin usarse.
El nivel 1 es un estado de transición: mucha gente lo alcanza al reorganizar una API antigua y se queda a medio camino. Reconocerlo tiene valor diagnóstico: si tus URLs son buenas pero todo es POST, estás aquí.
- Nivel 2: verbos HTTP y códigos de estado
Aquí está el salto grande, y donde vive la inmensa mayoría de las APIs profesionales, incluida la que construiremos en el módulo 3. Consiste en usar el protocolo tal como fue diseñado: el verbo indica la intención y el código de estado indica el resultado.
Crear un pedido:
POST /v1/pedidos HTTP/1.1
Host: api.tiendaaroma.example
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
{
"clienteId": "cli_842",
"lineas": [{ "cafeId": "caf_001", "cantidad": 2 }]
}HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/pedidos/ped_5001
Cache-Control: no-store
{
"id": "ped_5001",
"clienteId": "cli_842",
"estado": "pendiente_pago",
"totalEuros": 29.00,
"lineas": [
{ "cafeId": "caf_001", "nombre": "Etiopía Yirgacheffe", "cantidad": 2, "precioEuros": 14.50 }
],
"fechaCreacion": "2026-08-14T09:12:44Z"
}Fíjate en tres detalles que no existían en los niveles anteriores:
201 Createden lugar de200 OK: comunica con precisión que se ha creado algo nuevo.Location: dice dónde ha quedado el recurso, sin que el cliente tenga que componer la URL.Cache-Control: no-store: instrucción explícita para que nadie guarde datos de un pedido.
El resto de operaciones sobre el mismo recurso, ahora sin ningún campo accion:
# Consultar un pedido
curl -H "Authorization: Bearer TOKEN" \
https://api.tiendaaroma.example/v1/pedidos/ped_5001 # -> 200 OK
# Listar los pedidos de un cliente
curl -H "Authorization: Bearer TOKEN" \
"https://api.tiendaaroma.example/v1/pedidos?clienteId=cli_842" # -> 200 OK
# Cancelar un pedido
curl -X DELETE -H "Authorization: Bearer TOKEN" \
https://api.tiendaaroma.example/v1/pedidos/ped_5001 # -> 204 No ContentY los errores se expresan en el propio protocolo:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
"error": {
"codigo": "stock_insuficiente",
"mensaje": "No hay stock suficiente para completar el pedido",
"detalles": [
{ "cafeId": "caf_001", "solicitado": 2, "disponible": 0 }
]
}
}El código 422 dice "he entendido tu petición pero no puedo procesarla por su contenido"; el cuerpo da el detalle legible para el desarrollador. Máquinas y humanos, cada uno con su información.
Beneficios acumulados en el nivel 2:
- Caché real en los
GET, con impacto directo en coste y latencia. - Idempotencia en
GET,PUTyDELETE: los reintentos son seguros. - Monitorización automática: cualquier herramienta cuenta los 5xx sin saber nada de cafés.
- Curva de aprendizaje mínima para quien consume: si conoce HTTP, ya conoce tu API.
- Nivel 3: controles hipermedia (HATEOAS)
El último nivel añade enlaces a las respuestas: el servidor no solo devuelve datos, también indica qué transiciones son posibles desde el estado actual.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/pedidos/ped_5001
{
"id": "ped_5001",
"estado": "pendiente_pago",
"totalEuros": 29.00,
"lineas": [
{ "cafeId": "caf_001", "nombre": "Etiopía Yirgacheffe", "cantidad": 2, "precioEuros": 14.50 }
],
"_links": {
"self": { "href": "/v1/pedidos/ped_5001" },
"pagar": { "href": "/v1/pedidos/ped_5001/pago", "method": "POST" },
"cancelar": { "href": "/v1/pedidos/ped_5001", "method": "DELETE" },
"cliente": { "href": "/v1/clientes/cli_842" },
"lineas": { "href": "/v1/pedidos/ped_5001/lineas" }
}
}Lo interesante ocurre cuando el estado cambia. Una vez pagado y enviado, la misma petición GET /v1/pedidos/ped_5001 devuelve otros enlaces:
{
"id": "ped_5001",
"estado": "enviado",
"totalEuros": 29.00,
"_links": {
"self": { "href": "/v1/pedidos/ped_5001" },
"seguimiento":{ "href": "/v1/pedidos/ped_5001/envio" },
"factura": { "href": "/v1/pedidos/ped_5001/factura" },
"devolver": { "href": "/v1/pedidos/ped_5001/devolucion", "method": "POST" }
}
}Han desaparecido pagar y cancelar —ya no son posibles— y han aparecido seguimiento, factura y devolver. El cliente no necesita conocer la máquina de estados del pedido: le basta con pintar los botones correspondientes a los enlaces que recibe. Si mañana Tienda Aroma decide que los pedidos enviados también se pueden reenviar como regalo, aparece un enlace nuevo y los clientes que sepan interpretarlo lo mostrarán sin desplegar una versión nueva.
graph TD
N0["<b>Nivel 0</b><br/>El pantano de POX<br/><i>un endpoint, todo POST</i>"] --> N1
N1["<b>Nivel 1</b><br/>Recursos<br/><i>cada cosa con su URI</i>"] --> N2
N2["<b>Nivel 2</b><br/>Verbos y códigos HTTP<br/><i>GET/POST/PUT/DELETE + 2xx/4xx/5xx</i>"] --> N3
N3["<b>Nivel 3</b><br/>Controles hipermedia<br/><i>HATEOAS: enlaces que guían</i>"]
N2 -.- M["Aquí está la mayoría<br/>de las APIs profesionales"]
- Los cuatro niveles de un vistazo
| Nivel 0 | Nivel 1 | Nivel 2 | Nivel 3 | |
|---|---|---|---|---|
| URIs | Una sola | Una por recurso | Una por recurso | Una por recurso |
| Métodos | Solo POST |
Solo POST |
Todos, con su semántica | Todos, con su semántica |
| Códigos de estado | Siempre 200 |
Siempre 200 |
Los correctos | Los correctos |
| Caché | Imposible | Imposible | Sí, en lecturas | Sí, en lecturas |
| Descubrimiento | Documentación | Documentación | Documentación | Documentación + enlaces |
| Acoplamiento del cliente | Muy alto | Alto | Medio | Bajo |
| Coste de implementación | Bajo | Bajo | Medio | Alto |
| Ejemplo típico | SOAP, APIs antiguas | Reorganizaciones a medias | La mayoría de APIs actuales | APIs de pago, algunas públicas maduras |
- Qué es HATEOAS y qué problema resuelve
HATEOAS son las siglas de Hypermedia As The Engine Of Application State: la hipermedia como motor del estado de la aplicación. Traducido: el cliente avanza por la aplicación siguiendo los enlaces que el servidor le da, igual que tú navegas por una web sin conocer sus URLs de memoria.
La analogía con el navegador es la más clarificadora. Cuando entras en una tienda en línea:
- No escribes a mano
tienda.example/carrito/anadir?producto=123. - Pinchas en "Añadir al carrito", un enlace o un formulario que la propia página te ha dado.
- Si el producto está agotado, ese botón sencillamente no aparece.
El navegador no sabe nada de cafés ni de carritos: sabe seguir enlaces y enviar formularios. HATEOAS pretende llevar esa misma capacidad a los clientes programáticos.
El problema que resuelve es el acoplamiento a las URLs y a las reglas de negocio:
| Sin HATEOAS | Con HATEOAS |
|---|---|
| El cliente construye las URLs concatenando cadenas | El cliente sigue los enlaces recibidos |
| Cambiar una URL rompe a todos los clientes | Las URLs pueden cambiar sin romper nada |
| El cliente replica la máquina de estados ("si estado == 'pendiente_pago', muestra pagar") | El servidor decide y lo comunica con enlaces |
| Añadir una operación exige desplegar el cliente | La operación aparece como enlace nuevo |
- Formatos hipermedia: HAL, JSON:API y Siren
Si cada API inventa su propia forma de expresar enlaces, se pierde media ventaja. Por eso existen formatos estandarizados. Veamos el mismo pedido de Tienda Aroma en los dos más usados.
HAL (Hypertext Application Language)
Es el más ligero y el más adoptado. Añade dos convenciones: _links para los enlaces y _embedded para recursos incrustados. Su tipo de contenido es application/hal+json.
GET /v1/pedidos/ped_5001 HTTP/1.1
Accept: application/hal+json
HTTP/1.1 200 OK
Content-Type: application/hal+json{
"id": "ped_5001",
"estado": "pendiente_pago",
"totalEuros": 29.00,
"fechaCreacion": "2026-08-14T09:12:44Z",
"_links": {
"self": { "href": "/v1/pedidos/ped_5001" },
"cliente": { "href": "/v1/clientes/cli_842" },
"pagar": { "href": "/v1/pedidos/ped_5001/pago" },
"cancelar": { "href": "/v1/pedidos/ped_5001" }
},
"_embedded": {
"lineas": [
{
"cafeId": "caf_001",
"nombre": "Etiopía Yirgacheffe",
"cantidad": 2,
"precioEuros": 14.50,
"_links": { "cafe": { "href": "/v1/cafes/caf_001" } }
}
]
}
}Ventaja de HAL: es una capa muy fina sobre el JSON que ya tenías. Limitación: los enlaces no dicen qué método usar ni qué campos enviar; eso hay que documentarlo aparte o extenderlo por convención.
JSON:API
Es un formato mucho más estricto y completo, con especificación propia y tipo de contenido application/vnd.api+json. Estandariza no solo los enlaces, sino la estructura de datos, las relaciones, la inclusión de recursos relacionados, la paginación, el filtrado y los errores.
{
"data": {
"type": "pedidos",
"id": "ped_5001",
"attributes": {
"estado": "pendiente_pago",
"totalEuros": 29.00,
"fechaCreacion": "2026-08-14T09:12:44Z"
},
"relationships": {
"cliente": {
"data": { "type": "clientes", "id": "cli_842" },
"links": { "related": "/v1/clientes/cli_842" }
},
"lineas": {
"links": { "related": "/v1/pedidos/ped_5001/lineas" }
}
},
"links": {
"self": "/v1/pedidos/ped_5001",
"pagar": "/v1/pedidos/ped_5001/pago"
}
},
"included": [
{
"type": "clientes",
"id": "cli_842",
"attributes": { "nombre": "Marta García", "email": "[email protected]" }
}
]
}Diferencias apreciables frente a HAL: los datos van bajo data con type e id explícitos, los atributos se separan de las relaciones, y included permite enviar recursos relacionados completos en la misma respuesta (lo que ataca el mismo problema de "muchas peticiones" que motiva GraphQL). El precio es la verbosidad y una curva de aprendizaje real.
Siren
Va un paso más allá y modela acciones con todos sus detalles: método, tipo de contenido y campos esperados.
{
"class": ["pedido"],
"properties": { "id": "ped_5001", "estado": "pendiente_pago", "totalEuros": 29.00 },
"actions": [
{
"name": "pagar",
"title": "Pagar pedido",
"method": "POST",
"href": "/v1/pedidos/ped_5001/pago",
"type": "application/json",
"fields": [
{ "name": "metodoPago", "type": "text" },
{ "name": "tarjetaId", "type": "text" }
]
}
],
"links": [{ "rel": ["self"], "href": "/v1/pedidos/ped_5001" }]
}Con Siren, un cliente genérico podría generar un formulario a partir de la respuesta, igual que un navegador con HTML. Es el más fiel al espíritu de HATEOAS y el menos utilizado en la práctica.
Comparación
| Formato | Verbosidad | Estandariza acciones | Curva | Adopción |
|---|---|---|---|---|
| HAL | Baja | No (solo enlaces) | Suave | Alta |
| JSON:API | Alta | Parcialmente | Media-alta | Media, con buen ecosistema |
| Siren | Media | Sí, con campos | Media | Baja |
Existe además una alternativa mínima sin formato específico: la cabecera HTTP Link, estandarizada en el RFC 8288, muy usada para paginación:
Es hipermedia de bajo coste y perfectamente legítima. Volveremos a ella al hablar de paginación en 02-06.
- El debate honesto: ¿por qué casi nadie llega al nivel 3?
Fielding fue tajante en 2008: una API sin controles hipermedia no es REST. Y sin embargo, casi ninguna API comercial que uses a diario implementa HATEOAS de forma plena. Merece la pena entender por qué, sin caer ni en el dogmatismo ni en el desdén.
Argumentos a favor de HATEOAS:
- Desacopla al cliente de las URLs, permitiendo reorganizarlas sin romper nada.
- Centraliza la lógica de negocio en el servidor: la máquina de estados no se replica en cada cliente.
- Descubribilidad: un desarrollador nuevo puede explorar la API navegando desde la raíz.
- Es especialmente valioso con muchos clientes que no controlas y flujos con estados complejos.
Argumentos en contra, o al menos matizadores:
- Los clientes reales no son genéricos. Aroma Móvil tiene una pantalla diseñada específicamente para pagar un pedido; que el enlace
pagaresté o no presente no evita que la app tenga que conocer ese flujo, sus campos y su diseño. - No existe un cliente universal. El navegador funciona porque HTML define enlaces y formularios de forma estándar y hay un humano interpretando. Con JSON no hay equivalente: el cliente necesita saber que
rel: "pagar"significa pagar, lo que reintroduce acoplamiento semántico. - Coste de implementación y de mantenimiento en ambos lados: generar enlaces condicionados por estado y permisos no es trivial.
- Respuestas más pesadas, con impacto en clientes móviles.
- Los equipos consumidores suelen ignorar los enlaces y seguir construyendo URLs a mano, con lo que se paga el coste sin obtener el beneficio.
- Herramientas escasas: comparado con el ecosistema de OpenAPI, el soporte para hipermedia es reducido.
La postura del sector, que es también la que adopta este curso:
- El nivel 2 es el estándar profesional de facto. Una API en nivel 2 bien hecha —URIs limpias, verbos correctos, códigos de estado significativos, caché y errores bien modelados— es una API excelente.
- Añadir hipermedia parcial es barato y rentable: un enlace
self, enlaces de paginación y enlaces a los recursos relacionados aportan valor real con coste mínimo. Es lo que hace casi todo el mundo, y es lo que haremos con Tienda Aroma. - HATEOAS completo se reserva para casos con flujos de estado ricos y consumidores diversos: pasarelas de pago, banca abierta, APIs gubernamentales de larga vida.
- Criterios para decidir tu nivel
Preguntas concretas para situarte:
| Pregunta | Si la respuesta es sí... |
|---|---|
| ¿Controlas todos los clientes y sus despliegues? | Nivel 2 con enlaces self es suficiente |
| ¿Tus recursos tienen máquinas de estado ricas (pedidos, pagos, expedientes)? | Los enlaces por estado aportan mucho |
| ¿Tienes decenas de consumidores externos que no controlas? | Merece la pena invertir en hipermedia |
| ¿Prevés reorganizar las URLs en el futuro? | La hipermedia protege esa evolución |
| ¿Tus clientes son móviles con ancho de banda ajustado? | Cuidado con el peso extra de los enlaces |
| ¿Tienes ya documentación OpenAPI bien mantenida? | Cubre buena parte de la descubribilidad |
La decisión de Tienda Aroma: nivel 2 sólido con hipermedia selectiva. Concretamente:
- Enlace
selfen todos los recursos. - Enlaces de paginación en las colecciones (cabecera
Link). - Enlaces a recursos relacionados (
cliente,cafe) para no obligar a componer URLs. - Enlaces de acción condicionados al estado solo en pedidos, que es donde la máquina de estados es real y donde el panel interno y la app se benefician.
Es una decisión consciente, con sus motivos escritos. Eso es exactamente lo que se espera de un diseño profesional.
Errores Comunes y Consejos
- Tratar el modelo como un examen. Nadie premia llegar al nivel 3. Se premia una API que funcione bien y se pueda mantener.
- Creer que estás en nivel 2 porque usas
GETyPOST. Si devuelves200 OKen los errores o pones verbos en las URIs, no lo estás. - Implementar
_linkssin criterio. Devolver siempre los mismos enlaces, independientemente del estado y de los permisos, es decorativo: no aporta nada y añade peso. - Inventarse un formato hipermedia propio. Si vas a hacerlo, usa HAL o JSON:API: tendrás bibliotecas, documentación y desarrolladores que ya los conocen.
- Mezclar formatos. Usar
_linksde HAL junto con la estructuradata/attributesde JSON:API confunde a las herramientas y a las personas. - Confundir HATEOAS con "devolver URLs absolutas". Un campo
urlImagenno es un control hipermedia; un enlace con una relación (rel) que expresa una transición posible, sí. - Consejo: si empiezas hoy, apunta a un nivel 2 impecable y añade
selfy enlaces de paginación desde el primer día. Subir después es fácil; limpiar una API mal diseñada, no.
Ejercicios
Ejercicio 1: diagnosticar el nivel
Para cada API, indica en qué nivel de Richardson está y justifica con dos razones:
POST /serviciocon cuerpo{"metodo":"listarCafes"}, respuesta siempre200 OK.GET /v1/cafes/caf_001devuelve200 OK;POST /v1/cafesdevuelve201 CreatedconLocation;GET /v1/cafes/caf_999devuelve404 Not Found.POST /v1/cafes/buscar,POST /v1/cafes/crear,POST /v1/cafes/borrar, todas con200 OK.- Como la 2, pero cada respuesta incluye
_linksconself,resenasy, si hay stock,anadirAlCarrito.
Ejercicio 2: subir un nivel
Esta operación de Tienda Aroma está en nivel 1. Reescríbela en nivel 2 completo (petición, código de estado y cabeceras relevantes), y explica cada decisión.
POST /v1/carritos/car_77 HTTP/1.1
Content-Type: application/json
{ "accion": "eliminarLinea", "cafeId": "caf_002" }Respuesta actual: 200 OK con {"exito": true}.
Ejercicio 3: diseñar controles hipermedia por estado
El recurso reseña de Tienda Aroma tiene tres estados: pendiente_moderacion, publicada y rechazada. Las reglas son:
- Una reseña pendiente puede aprobarse o rechazarse (solo un moderador), y su autor puede editarla.
- Una reseña publicada puede responderse por el equipo de la tienda y su autor puede borrarla.
- Una reseña rechazada no admite ninguna acción, pero se puede ver el motivo.
Diseña la respuesta en formato HAL para los tres estados, vista por un moderador.
Soluciones
Solución 1
- Nivel 0. Un único endpoint (
/servicio), la operación va en el cuerpo, todo porPOSTy siempre200. - Nivel 2. Hay URIs por recurso, se usan los métodos según su semántica y los códigos de estado son correctos (
201conLocation,404cuando no existe). No hay enlaces, así que no es nivel 3. - Nivel 1. Existen URIs bajo
/v1/cafes, pero el verbo sigue estando en la ruta y todo va porPOSTcon200: no se aprovechan ni los métodos ni los códigos. - Nivel 3. Cumple todo lo del nivel 2 y además incorpora controles hipermedia condicionados por el estado (el enlace de añadir al carrito solo aparece si hay stock).
Solución 2
DELETE /v1/carritos/car_77/lineas/caf_002 HTTP/1.1
Host: api.tiendaaroma.example
Authorization: Bearer TOKENDecisiones:
- El verbo pasa al método HTTP: eliminar es
DELETE, no un campoaccionen el cuerpo. - La línea del carrito se convierte en un recurso propio con URI identificable (
/v1/carritos/car_77/lineas/caf_002), lo que también permite consultarla o modificar su cantidad conPATCH. DELETEes idempotente: si la petición se reintenta tras un fallo de red, el resultado es el mismo. Con elPOSTanterior no había esa garantía.204 No Contentindica éxito sin cuerpo, en lugar de un{"exito": true}redundante. Si quisiéramos devolver el carrito actualizado para ahorrar una petición al cliente,200 OKcon el carrito completo también sería correcto: es una decisión de diseño legítima.- Si la línea no existe, se responde
404 Not Found; si el carrito es de otro cliente,403 Forbidden.
Solución 3
Estado pendiente_moderacion (vista de moderador):
{
"id": "res_101",
"cafeId": "caf_001",
"autor": "Marta G.",
"puntuacion": 5,
"comentario": "Equilibrado y dulce.",
"estado": "pendiente_moderacion",
"_links": {
"self": { "href": "/v1/resenas/res_101" },
"cafe": { "href": "/v1/cafes/caf_001" },
"aprobar": { "href": "/v1/resenas/res_101/aprobacion", "method": "POST" },
"rechazar": { "href": "/v1/resenas/res_101/rechazo", "method": "POST" }
}
}Estado publicada:
{
"id": "res_101",
"estado": "publicada",
"fechaPublicacion": "2026-08-14T10:02:00Z",
"_links": {
"self": { "href": "/v1/resenas/res_101" },
"cafe": { "href": "/v1/cafes/caf_001" },
"responder": { "href": "/v1/resenas/res_101/respuestas", "method": "POST" }
}
}Estado rechazada:
{
"id": "res_101",
"estado": "rechazada",
"motivoRechazo": "Contiene lenguaje ofensivo",
"_links": {
"self": { "href": "/v1/resenas/res_101" },
"cafe": { "href": "/v1/cafes/caf_001" }
}
}Puntos clave de la solución:
- Los enlaces cambian con el estado: ahí está el valor de HATEOAS. El panel de moderación puede pintar sus botones a partir de los enlaces sin conocer la máquina de estados.
- Los enlaces también dependen de quién pregunta: el autor vería
editaryborraren lugar deaprobaryrechazar. Una respuesta hipermedia refleja permisos, no solo estado. - Las acciones se modelan como subrecursos (
/aprobacion,/rechazo,/respuestas) a los que se hacePOST, evitando verbos en las URIs. - El enlace
selfestá siempre, ymotivoRechazosolo aparece cuando tiene sentido.
Conclusión
El modelo de madurez de Richardson ofrece un vocabulario preciso para hablar de cuán RESTful es una API: del nivel 0 —un endpoint, todo POST, HTTP como mero túnel— al nivel 3, donde las respuestas incluyen los controles hipermedia que guían al cliente. Hemos visto la misma operación de Tienda Aroma reescrita en los cuatro niveles y comprobado que el salto decisivo es el nivel 2: recursos con URI propia, verbos con su semántica y códigos de estado correctos, que es donde se obtienen caché, idempotencia y monitorización gratuitas. HATEOAS aporta desacoplamiento real y centraliza la máquina de estados, pero tiene un coste que muchos equipos no rentabilizan; por eso Tienda Aroma adoptará un nivel 2 sólido con hipermedia selectiva, y con los motivos escritos.
Con el modelo REST ya comprendido a fondo, toca situarlo frente a las alternativas. En la siguiente lección, REST vs. SOAP, veremos de cerca el estilo que dominó los servicios web empresariales: su sobre XML, su contrato WSDL, su pila WS-*, y compararemos ambos enfoques lado a lado sobre el mismo caso de Tienda Aroma para entender cuándo cada uno sigue teniendo sentido hoy.
Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful
Módulo 1: Introducción a las APIs RESTful
- ¿Qué es una API?
- Historia y evolución de las APIs
- Fundamentos de HTTP para APIs
- Principios básicos de REST
- Modelo de madurez de Richardson y HATEOAS
- REST vs. SOAP
- REST frente a GraphQL, gRPC y webhooks
Módulo 2: Diseño de APIs RESTful
- Principios de diseño de APIs RESTful
- Recursos y URIs
- Métodos HTTP
- Códigos de estado HTTP
- Representaciones, cabeceras y negociación de contenido
- Filtrado, ordenación, paginación y búsqueda
- Versionado de APIs
- Documentación de APIs
Módulo 3: Desarrollo de APIs RESTful
- Configuración del entorno de desarrollo
- Creación de un servidor básico
- Manejo de peticiones y respuestas
- Validación de datos de entrada
- Persistencia y capa de acceso a datos
- Autenticación y autorización
- Manejo de errores
- Pruebas y validación
Módulo 4: Buenas Prácticas y Seguridad
- Buenas prácticas en el diseño de APIs
- Seguridad en APIs RESTful
- OAuth 2.0 y OpenID Connect en la práctica
- Rate limiting y throttling
- CORS y políticas de seguridad
- Caché HTTP y rendimiento
- Observabilidad: logs, métricas y trazas
Módulo 5: Herramientas y Frameworks
- Postman para pruebas de APIs
- Swagger y OpenAPI para documentación
- Frameworks populares para APIs RESTful
- Contratos, mocks y pruebas automatizadas de API
- Integración continua y despliegue
- API gateways y portales de desarrollador
