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

  1. Para qué sirve el modelo de madurez
  2. Nivel 0: el pantano de POX
  3. Nivel 1: recursos
  4. Nivel 2: verbos HTTP y códigos de estado
  5. Nivel 3: controles hipermedia (HATEOAS)
  6. Los cuatro niveles de un vistazo
  7. Qué es HATEOAS y qué problema resuelve
  8. Formatos hipermedia: HAL, JSON:API y Siren
  9. El debate honesto: ¿por qué casi nadie llega al nivel 3?
  10. Criterios para decidir tu nivel

  1. 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:

  1. No es de Fielding ni es normativo. Es un modelo descriptivo, útil para diagnosticar, no un examen que haya que aprobar.
  2. Subir de nivel no es automáticamente mejor. Cada nivel tiene un coste. El objetivo es elegir con criterio, no maximizar la puntuación.

  1. 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.

  1. 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:

POST /v1/pedidos/ped_5001 HTTP/1.1
Content-Type: application/json

{ "accion": "obtener" }

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 POST para 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í.

  1. 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 Created en lugar de 200 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 Content

Y 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, PUT y DELETE: 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.

  1. 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"]

  1. 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

  1. 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

  1. 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:

Link: </v1/cafes?pagina=3>; rel="next", </v1/cafes?pagina=1>; rel="first"

Es hipermedia de bajo coste y perfectamente legítima. Volveremos a ella al hablar de paginación en 02-06.

  1. 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 pagar esté 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.

  1. 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 self en 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 GET y POST. Si devuelves 200 OK en los errores o pones verbos en las URIs, no lo estás.
  • Implementar _links sin 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 _links de HAL junto con la estructura data/attributes de JSON:API confunde a las herramientas y a las personas.
  • Confundir HATEOAS con "devolver URLs absolutas". Un campo urlImagen no 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 self y 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:

  1. POST /servicio con cuerpo {"metodo":"listarCafes"}, respuesta siempre 200 OK.
  2. GET /v1/cafes/caf_001 devuelve 200 OK; POST /v1/cafes devuelve 201 Created con Location; GET /v1/cafes/caf_999 devuelve 404 Not Found.
  3. POST /v1/cafes/buscar, POST /v1/cafes/crear, POST /v1/cafes/borrar, todas con 200 OK.
  4. Como la 2, pero cada respuesta incluye _links con self, resenas y, 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

  1. Nivel 0. Un único endpoint (/servicio), la operación va en el cuerpo, todo por POST y siempre 200.
  2. Nivel 2. Hay URIs por recurso, se usan los métodos según su semántica y los códigos de estado son correctos (201 con Location, 404 cuando no existe). No hay enlaces, así que no es nivel 3.
  3. Nivel 1. Existen URIs bajo /v1/cafes, pero el verbo sigue estando en la ruta y todo va por POST con 200: no se aprovechan ni los métodos ni los códigos.
  4. 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 TOKEN
HTTP/1.1 204 No Content

Decisiones:

  • El verbo pasa al método HTTP: eliminar es DELETE, no un campo accion en 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 con PATCH.
  • DELETE es idempotente: si la petición se reintenta tras un fallo de red, el resultado es el mismo. Con el POST anterior no había esa garantía.
  • 204 No Content indica é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 OK con 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 editar y borrar en lugar de aprobar y rechazar. Una respuesta hipermedia refleja permisos, no solo estado.
  • Las acciones se modelan como subrecursos (/aprobacion, /rechazo, /respuestas) a los que se hace POST, evitando verbos en las URIs.
  • El enlace self está siempre, y motivoRechazo solo 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

Módulo 2: Diseño de APIs RESTful

Módulo 3: Desarrollo de APIs RESTful

Módulo 4: Buenas Prácticas y Seguridad

Módulo 5: Herramientas y Frameworks

Módulo 6: Casos de Estudio y Proyectos

© Copyright 2026. Todos los derechos reservados