Cerramos el módulo anterior con el contenedor de Spring completamente desmontado: sabemos cómo nacen los beans, cómo se inyectan, de dónde salen sus propiedades y por qué aparecen solos. Todo eso ocurre por debajo, donde nadie lo ve. A partir de ahora trabajamos en la capa que sí se ve: la API HTTP que consumirán la aplicación móvil de CicloUrbana, el panel de control de los operarios de Ribalta y, eventualmente, el portal de datos abiertos del ayuntamiento. Esta primera lección no escribe controladores —eso empieza en la siguiente—, sino que establece el marco: qué significa REST realmente, cómo se diseñan los recursos y las URLs, qué verbo y qué código de estado corresponden a cada operación, cómo se negocia el formato de la respuesta y cómo se versiona un contrato público. Las decisiones que tomemos aquí condicionan las seis lecciones siguientes y, en un sistema real, son muy caras de revertir: una URL publicada es una promesa.

Contenido

  1. Qué es REST y de dónde viene
  2. Las seis restricciones de REST y lo que implican en la práctica
  3. Recursos, identificadores y representaciones
  4. El modelo de madurez de Richardson
  5. Diseño de las URLs de CicloUrbana
  6. El contrato completo de la API del módulo
  7. Verbos HTTP: seguridad e idempotencia
  8. Códigos de estado HTTP
  9. Negociación de contenido
  10. Versionado de la API
  11. REST frente a SOAP, GraphQL y gRPC
  12. Dónde encaja Spring MVC: el DispatcherServlet
  13. Errores Comunes y Consejos
  14. Ejercicios

  1. Qué es REST y de dónde viene

REST —REpresentational State Transfer— no es un protocolo, ni una librería, ni un formato. Es un estilo arquitectónico descrito por Roy Fielding en el capítulo 5 de su tesis doctoral de 2000, Architectural Styles and the Design of Network-based Software Architectures. Fielding no inventó REST observando cómo se hacían las APIs de su época: lo describió analizando por qué la Web había funcionado a una escala que ningún sistema distribuido anterior había alcanzado.

Ese origen explica muchas cosas. REST no es "JSON sobre HTTP con URLs bonitas". Es un conjunto de restricciones que, aplicadas a un sistema distribuido, le confieren ciertas propiedades deseables: escalabilidad, independencia entre cliente y servidor, tolerancia a la evolución y capacidad de intercalar intermediarios (cachés, proxies, balanceadores) que entienden el tráfico sin conocer la aplicación.

La distinción importante para un desarrollador es esta:

Afirmación ¿Cierta?
REST exige JSON No. REST no menciona ningún formato. JSON es la elección habitual, no un requisito.
REST exige HTTP No formalmente, pero HTTP es la única implementación relevante y la que asumiremos siempre.
REST exige URLs con sustantivos No literalmente, pero es la consecuencia natural de la restricción de interfaz uniforme.
Una API con URLs limpias y JSON ya es REST No necesariamente. Suele quedarse en el nivel 2 de madurez (apartado 4).
REST es más rápido que SOAP No por definición. Es más simple y más cacheable, que a menudo se traduce en rapidez.

En la práctica del sector, "API REST" se usa como sinónimo de "API HTTP orientada a recursos con JSON". CicloUrbana será exactamente eso, con la conciencia de qué restricciones cumplimos y cuáles no.

  1. Las seis restricciones de REST y lo que implican en la práctica

Fielding define seis restricciones. Cinco son obligatorias y una es opcional. Lo interesante no es memorizarlas, sino ver qué obliga cada una en el código que escribiremos.

Cliente-servidor

Separación de responsabilidades: el cliente se ocupa de la interfaz de usuario, el servidor de los datos y las reglas de negocio. Se comunican solo mediante el contrato de la API.

Qué implica en CicloUrbana: el backend nunca genera HTML ni sabe si el cliente es una app Android o un panel web. Si mañana el ayuntamiento quiere un tótem táctil en cada estación, no se toca el servidor. Esto también significa que la lógica de negocio (¿se puede alquilar una bicicleta con el 15% de batería?) vive en el servidor, no en la app: un cliente puede estar desactualizado o ser malicioso.

Sin estado (stateless)

Cada petición contiene toda la información necesaria para ser procesada. El servidor no guarda contexto de sesión entre peticiones.

Qué implica: nada de HttpSession con el carrito del usuario, el paso del asistente o el identificador del alquiler en curso. Si el cliente necesita autenticarse, envía la credencial —un token— en cada petición (módulo 5). Si necesita paginar, envía el número de página cada vez.

El beneficio es la escalabilidad horizontal: si CicloUrbana crece y desplegamos tres instancias tras un balanceador, cualquiera puede atender cualquier petición. Con estado en memoria haría falta afinidad de sesión o una sesión replicada, y ambas cosas complican el despliegue (lo veremos al contenerizar en 07-04).

El coste es que las peticiones son más grandes y a veces se repite trabajo (validar el token en cada llamada). Es un intercambio casi siempre favorable.

Cacheable

Cada respuesta debe indicar, explícita o implícitamente, si puede almacenarse en caché y durante cuánto tiempo.

Qué implica: GET /api/v1/estaciones devuelve datos que cambian poco —una estación nueva al mes— y puede llevar Cache-Control: public, max-age=300. En cambio GET /api/v1/estaciones/1/bicicletas cambia cada minuto y debe ir con Cache-Control: no-store o un max-age muy corto. Esta decisión, tomada bien, quita más carga al servidor que cualquier optimización de código (volveremos a ello en 09-02).

Interfaz uniforme

Es la restricción central y la que más distingue REST. Se descompone en cuatro sub-restricciones:

Sub-restricción Significado En CicloUrbana
Identificación de recursos Cada cosa tiene su URI /api/v1/estaciones/1 identifica la Plaza Mayor
Manipulación mediante representaciones El cliente modifica enviando una representación, no invocando métodos PUT con el JSON completo de la estación
Mensajes autodescriptivos Cada mensaje lleva la información para interpretarlo Content-Type: application/json, códigos de estado
HATEOAS La respuesta incluye enlaces a las transiciones posibles Opcional; ver nivel 3 en el apartado 4

Sistema por capas

El cliente no sabe si habla con el servidor final o con un intermediario. Entre la app y nuestro Tomcat puede haber una CDN, un balanceador, un API gateway y un proxy de seguridad, y nada cambia.

Qué implica: no asumir nunca la IP del cliente sin mirar X-Forwarded-For, no confiar en que el puerto que ve la aplicación es el que usó el cliente, y generar las URLs absolutas con cuidado (lo veremos con la cabecera Location en 03-03).

Código bajo demanda (opcional)

El servidor puede enviar código ejecutable al cliente. Es la única restricción opcional y en APIs de datos prácticamente no se usa. CicloUrbana no la aplicará.

  1. Recursos, identificadores y representaciones

Tres conceptos que se confunden constantemente y conviene separar con precisión.

  • Recurso: cualquier concepto del dominio que merezca ser nombrado. "La estación de la Plaza Mayor", "las bicicletas disponibles en la estación 3", "el alquiler número 4471". Es una noción abstracta, no una fila de una tabla.
  • Identificador (URI): el nombre estable del recurso. /api/v1/estaciones/1.
  • Representación: una forma concreta de mostrar el estado del recurso en un momento dado. El mismo recurso puede tener varias representaciones: JSON, XML, CSV, una versión resumida y otra detallada.
graph LR
    R["Recurso<br/>«La estación Plaza Mayor»"] -->|se identifica con| U["/api/v1/estaciones/1"]
    R -->|se representa como| J["application/json<br/>{ id, nombre, capacidad... }"]
    R -->|se representa como| C["text/csv<br/>1,Plaza Mayor,24"]

La consecuencia práctica es importante: el recurso no es la clase Java. Nuestro record Estacion es una representación interna; lo que la API expone es otra cosa, que puede omitir campos, añadir campos calculados o agregar datos de varias fuentes. Esa separación es justamente el tema de la lección 03-05 (DTOs).

Un ejemplo concreto de CicloUrbana: el recurso "estación" en la vista de listado incluirá bicicletasDisponibles, un número calculado que no está en el record Estacion. Y no incluirá las coordenadas exactas del anclaje de mantenimiento, que sí están en el modelo interno. Recurso y clase son cosas distintas.

  1. El modelo de madurez de Richardson

Leonard Richardson propuso en 2008 una escala de cuatro niveles para medir cuánto se aproxima una API a REST. Es la herramienta más útil para diagnosticar una API existente.

Nivel Nombre Qué usa Ejemplo en CicloUrbana
0 El pantano del POX Una URL, un verbo POST /api/servicio con {"operacion":"listarEstaciones"}
1 Recursos Varias URLs, un verbo POST /api/estaciones/listar, POST /api/estaciones/1/borrar
2 Verbos HTTP URLs + verbos + códigos de estado GET /api/v1/estaciones, DELETE /api/v1/estaciones/1 → 204
3 HATEOAS Lo anterior + enlaces en las respuestas La estación devuelve _links.bicicletas y _links.alquilar

Veámoslos con peticiones concretas.

Nivel 0 — todo pasa por un único punto de entrada y el verbo HTTP no significa nada:

POST /api/servicio HTTP/1.1
Content-Type: application/json

{ "operacion": "obtenerEstacion", "parametros": { "id": 1 } }

La respuesta será 200 OK incluso si la estación no existe, con un {"error": "no encontrada"} en el cuerpo. HTTP se usa como simple túnel. Es el modelo de SOAP y de muchas APIs internas heredadas.

Nivel 1 — hay recursos con URL propia, pero las operaciones siguen siendo verbos en la ruta:

POST /api/estaciones/1/obtener
POST /api/estaciones/1/borrar
POST /api/estaciones/crear

Se gana legibilidad, pero un intermediario sigue sin poder cachear nada ni saber qué peticiones son seguras.

Nivel 2 — el verbo HTTP expresa la operación y el código de estado expresa el resultado:

GET    /api/v1/estaciones/1      → 200 OK
DELETE /api/v1/estaciones/1      → 204 No Content
DELETE /api/v1/estaciones/999    → 404 Not Found

Ahora una CDN sabe que puede cachear el GET, un proxy sabe que puede reintentar el DELETE sin efectos secundarios y un cliente genérico entiende el resultado sin leer el cuerpo. Este es el nivel en el que opera la inmensa mayoría de las APIs profesionales, y el que implementará CicloUrbana.

Nivel 3 — las respuestas describen qué se puede hacer a continuación:

{
  "id": 1,
  "nombre": "Plaza Mayor",
  "bicicletasDisponibles": 7,
  "_links": {
    "self":       { "href": "/api/v1/estaciones/1" },
    "bicicletas": { "href": "/api/v1/estaciones/1/bicicletas" },
    "alquilar":   { "href": "/api/v1/alquileres", "method": "POST" }
  }
}

La idea es potente: si la estación está vacía, el enlace alquilar simplemente no aparece y el cliente no necesita replicar esa regla de negocio. Solo el nivel 3 cumple la restricción de interfaz uniforme completa, y solo él merece el nombre de REST según Fielding.

Por qué CicloUrbana se queda en el nivel 2. El nivel 3 exige clientes que naveguen enlaces en lugar de construir URLs, y en la práctica casi ningún cliente lo hace: las apps móviles llevan las rutas escritas en el código. El coste de mantener los enlaces no se compensa. Es una decisión consciente e informada, que es exactamente lo que se espera de un diseñador de APIs: conocer el nivel 3, aplicarlo cuando aporta —flujos con máquina de estados compleja— y no por dogma. Spring ofrece spring-boot-starter-hateoas si algún día se necesita.

  1. Diseño de las URLs de CicloUrbana

Las reglas que seguirá el proyecto, con su justificación:

Sustantivos en plural, nunca verbos. /estaciones, no /obtenerEstaciones ni /estacion. El plural es coherente: /estaciones es la colección y /estaciones/1 un elemento de ella. Usar el singular obliga a decidir caso por caso y produce incoherencias.

Minúsculas y guiones para separar palabras. /api/v1/tipos-de-usuario, no /tiposDeUsuario ni /tipos_de_usuario. Los nombres de dominio no distinguen mayúsculas pero las rutas sí, y el guion es la convención de la web.

Jerarquía para expresar pertenencia. /api/v1/estaciones/1/bicicletas son las bicicletas de la estación 1. La regla práctica: no anidar más de dos niveles. /estaciones/1/bicicletas/42/alquileres/7 es ilegible; si necesitas el alquiler 7, pídelo por su URL propia, /alquileres/7.

Sin extensiones ni sufijos de formato. Nada de /estaciones.json. El formato se negocia con cabeceras (apartado 9).

Sin barra final. /api/v1/estaciones y /api/v1/estaciones/ deben ser la misma cosa; elegimos la primera forma.

Los filtros van en la cadena de consulta, no en la ruta. /api/v1/estaciones?capacidadMinima=20&ciudad=ribalta, no /api/v1/estaciones/capacidad-minima/20. La razón: la ruta identifica qué recurso; los parámetros modifican cómo se devuelve.

Bien Mal Por qué
GET /api/v1/estaciones GET /api/v1/getEstaciones El verbo ya está en el método HTTP
GET /api/v1/estaciones/1 GET /api/v1/estacion?id=1 Un elemento tiene URI propia
GET /api/v1/estaciones?activa=true GET /api/v1/estaciones/activas activas no es un recurso, es un filtro
GET /api/v1/estaciones/1/bicicletas GET /api/v1/bicicletas?estacion=1 Ambas valen; la primera expresa mejor la pertenencia
DELETE /api/v1/estaciones/1 POST /api/v1/estaciones/1/eliminar El verbo HTTP es la operación

Sobre el prefijo /api: separa la API de cualquier contenido estático o página web que la aplicación pueda servir en el mismo dominio, y facilita las reglas del proxy inverso y de CORS. Es una convención tan extendida que su ausencia sorprende.

  1. El contrato completo de la API del módulo

Este es el objetivo del módulo 3. Al terminar la lección 03-07, CicloUrbana expondrá exactamente esto:

Verbo Ruta Descripción Éxito Lección
GET /api/v1/estaciones Listado con filtros y paginación 200 03-02
GET /api/v1/estaciones/{id} Detalle de una estación 200 03-02
POST /api/v1/estaciones Alta de estación 201 + Location 03-03
PUT /api/v1/estaciones/{id} Reemplazo total 200 03-03
PATCH /api/v1/estaciones/{id} Actualización parcial 200 03-03
DELETE /api/v1/estaciones/{id} Baja de estación 204 03-03
GET /api/v1/estaciones/{id}/bicicletas Bicicletas ancladas en la estación 200 03-03
GET /api/v1/bicicletas Listado de bicicletas de la red 200 03-03
GET /api/v1/bicicletas/{id} Detalle de una bicicleta 200 03-03
POST /api/v1/bicicletas Alta de bicicleta 201 + Location 03-03
POST /api/v1/alquileres Iniciar un alquiler 201 + Location 03-03
POST /api/v1/alquileres/{id}/finalizar Finalizar un alquiler 200 03-03
GET /api/v1/alquileres/{id} Detalle de un alquiler 200 03-03

Trece endpoints. Ninguno lleva un verbo en la ruta salvo finalizar, que es una transición de estado y no una operación CRUD; el apartado correspondiente de 03-03 justifica esa excepción con detalle.

Fíjate en algo que se hará evidente en 03-05: GET /api/v1/estaciones y GET /api/v1/estaciones/{id} devuelven representaciones distintas del mismo recurso. El listado devuelve un resumen; el detalle incluye además la lista de bicicletas ancladas. Esto es legítimo y muy común: un mismo recurso, dos representaciones con distinto nivel de detalle.

  1. Verbos HTTP: seguridad e idempotencia

Dos propiedades definidas en el RFC 9110 gobiernan qué puede hacer un intermediario con cada verbo.

  • Seguro (safe): no modifica el estado del servidor. Un buscador puede recorrer todos los GET de una API sin romper nada.
  • Idempotente: ejecutarlo N veces deja el sistema en el mismo estado que ejecutarlo una vez. Esto permite reintentar sin miedo cuando la red falla.
Verbo Seguro Idempotente Cuerpo en la petición Uso en CicloUrbana
GET Sí Sí No Consultar estaciones, bicicletas, alquileres
HEAD Sí Sí No Comprobar existencia sin descargar el cuerpo
OPTIONS Sí Sí No Descubrir verbos permitidos; usado por CORS
POST No No Sí Crear estación, iniciar alquiler
PUT No Sí Sí Reemplazar una estación completa
PATCH No No garantizada Sí Modificar campos sueltos de una estación
DELETE No Sí Opcional Dar de baja una estación

La idempotencia es más práctica de lo que parece. Imagina la app de CicloUrbana en el metro de Ribalta: el usuario pulsa "alquilar", el POST sale, el servidor lo procesa, y la respuesta se pierde porque el móvil entra en un túnel. La app reintenta. Si el POST no es idempotente, el ciudadano acaba con dos alquileres y dos cobros.

Que POST no sea idempotente es una característica, no un defecto: cada POST /api/v1/alquileres crea un alquiler nuevo, y eso es lo correcto. Para evitar duplicados por reintento existe el patrón de clave de idempotencia —una cabecera Idempotency-Key con un UUID generado por el cliente— que el servidor recuerda para no procesar dos veces la misma petición. Es el mecanismo que usan las pasarelas de pago. En CicloUrbana lo mencionamos aquí y no lo implementamos: exige almacenamiento persistente, que llega en el módulo 4.

Que DELETE sea idempotente tiene una consecuencia de diseño concreta: DELETE /api/v1/estaciones/1 sobre una estación ya borrada debería devolver 204 o 404, pero el estado final es el mismo —la estación no existe—, y eso es lo que define la idempotencia. Volveremos a ello en 03-03.

PATCH no es idempotente en general porque su cuerpo puede describir una operación relativa: "incrementa la capacidad en 4" da un resultado distinto cada vez. Si el cuerpo describe valores absolutos —"la capacidad pasa a ser 28"— sí lo es. Es responsabilidad de quien diseña la API decidirlo y documentarlo.

  1. Códigos de estado HTTP

El código de estado es la parte más ignorada y más valiosa de una respuesta HTTP. Devolver 200 OK con {"exito": false} dentro obliga a todo cliente a leer el cuerpo para saber si algo funcionó, y rompe a cualquier intermediario.

Las cinco familias:

Familia Significado Quién tiene el problema
1xx Informativo Nadie; poco usado
2xx Éxito Nadie
3xx Redirección El cliente debe ir a otro sitio
4xx Error del cliente El cliente: petición mal formada, no autorizada, recurso inexistente
5xx Error del servidor Nosotros: bug, base de datos caída, dependencia no disponible

La distinción 4xx/5xx no es cosmética. Los sistemas de monitorización (módulo 9) alertan sobre los 5xx y no sobre los 4xx, porque un 404 es funcionamiento normal y un 500 es una llamada a las tres de la mañana. Devolver 500 cuando el cliente ha enviado un JSON inválido genera ruido y desgasta la confianza en las alertas.

Los códigos que usará CicloUrbana:

Código Nombre Cuándo lo devuelve CicloUrbana
200 OK Consulta correcta, PUT/PATCH que devuelve el recurso actualizado
201 Created Estación, bicicleta o alquiler creados; siempre con cabecera Location
204 No Content DELETE correcto; PUT que no devuelve cuerpo
304 Not Modified Respuesta a un GET condicional con ETag coincidente (03-03)
400 Bad Request JSON mal formado o validación fallida (03-04)
401 Unauthorized Falta el token o es inválido (módulo 5)
403 Forbidden Autenticado pero sin permiso (módulo 5)
404 Not Found La estación 999 no existe
405 Method Not Allowed DELETE /api/v1/estaciones sobre la colección
409 Conflict Matrícula duplicada, estación llena al devolver una bicicleta
412 Precondition Failed If-Match con un ETag obsoleto (03-03)
415 Unsupported Media Type Se envía XML donde se espera JSON
422 Unprocessable Entity Sintaxis correcta pero regla de negocio violada (03-04)
500 Internal Server Error Excepción no controlada; nunca debe filtrar detalles (03-06)
503 Service Unavailable Dependencia caída, arranque en curso (módulo 7)

Dos confusiones frecuentes que conviene resolver ya:

  • 401 frente a 403. 401 significa "no sé quién eres" (falta autenticación o es inválida); 403 significa "sé quién eres y no puedes" (falta autorización). Los nombres del estándar son desafortunados: Unauthorized es en realidad no autenticado.
  • 400 frente a 422. 400 es "no entiendo tu petición" (JSON roto, campo obligatorio ausente, tipo incorrecto); 422 es "te entiendo perfectamente, pero lo que pides no es aceptable" (quieres alquilar una bicicleta que está en mantenimiento). La lección 03-04 fija la política del proyecto.

  1. Negociación de contenido

HTTP permite que el mismo recurso se sirva en varios formatos y que cliente y servidor acuerden cuál. El mecanismo es un par de cabeceras:

  • Content-Type: describe el formato del cuerpo que se está enviando. Lo pone quien envía el cuerpo, sea el cliente en un POST o el servidor en la respuesta.
  • Accept: es la lista de formatos que el cliente sabe interpretar, en orden de preferencia. Lo pone siempre el cliente.
POST /api/v1/estaciones HTTP/1.1
Host: api.ciclourbana.ribalta.es
Content-Type: application/json
Accept: application/json

{ "nombre": "Mercado Central", "capacidad": 20 }

Aquí el cliente dice: "te envío JSON y quiero JSON de vuelta". El servidor responde:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/estaciones/5

Los tipos MIME relevantes para el curso:

Tipo MIME Uso
application/json El formato por defecto de toda la API de CicloUrbana
application/problem+json Respuestas de error con RFC 7807 Problem Details (03-06)
application/x-www-form-urlencoded Formularios HTML clásicos; no lo usa esta API
multipart/form-data Subida de ficheros (fotos de incidencia en las bicicletas)
text/csv Exportación de datos para el ayuntamiento
application/vnd.ciclourbana.v2+json Versionado por media type (apartado 10)

La cabecera Accept admite pesos de preferencia:

Accept: application/json;q=0.9, text/csv;q=0.5, */*;q=0.1

El cliente prefiere JSON, acepta CSV y, en último caso, cualquier cosa. Si el servidor no puede satisfacer ninguna de las opciones, responde 406 Not Acceptable. Si el cliente envía un Content-Type que el servidor no sabe leer, la respuesta es 415 Unsupported Media Type. Son dos códigos simétricos que se confunden a menudo: 406 mira Accept, 415 mira Content-Type.

Spring implementa todo esto automáticamente a partir de los atributos produces y consumes de @RequestMapping, que veremos en la lección siguiente.

  1. Versionado de la API

Una API pública es un contrato. En cuanto la app móvil de CicloUrbana esté publicada en las tiendas, habrá ciudadanos de Ribalta con versiones antiguas instaladas durante meses. Cambiar el nombre de un campo rompe sus teléfonos.

La regla básica es distinguir cambios compatibles de incompatibles:

Compatible (no exige nueva versión) Incompatible (exige nueva versión)
Añadir un campo opcional a una respuesta Eliminar o renombrar un campo
Añadir un endpoint nuevo Cambiar el tipo de un campo
Añadir un parámetro de consulta opcional Hacer obligatorio un campo que no lo era
Añadir un valor a un enum de respuesta Cambiar el significado de un campo
Relajar una validación Cambiar un código de estado de éxito

Las tres estrategias de versionado:

Por URL — /api/v1/estaciones, /api/v2/estaciones.

GET /api/v1/estaciones HTTP/1.1

Ventajas: visible de un vistazo, trivial de probar en un navegador o con curl, fácil de enrutar en un proxy o de desplegar como aplicaciones separadas, evidente en los logs. Inconveniente teórico: dos URLs distintas para el mismo recurso, lo que rompe la idea de identificador único.

Por cabecera personalizada — el cliente envía X-API-Version: 1.

GET /api/estaciones HTTP/1.1
X-API-Version: 2

Ventaja: la URI del recurso es una sola. Inconvenientes: invisible en el log de acceso salvo configuración extra, imposible de probar pegando una URL en el navegador, y la caché HTTP necesita Vary: X-API-Version para no servir la versión equivocada.

Por media type (content negotiation de versión) — la variante más purista.

GET /api/estaciones HTTP/1.1
Accept: application/vnd.ciclourbana.v2+json

Ventaja: teóricamente la más correcta; la versión forma parte de la representación, no de la identidad. Inconvenientes: la más difícil de explicar a un equipo cliente, la más incómoda de probar y la que peor soportan las herramientas.

Estrategia Visibilidad Facilidad de prueba Pureza REST Adopción real
URL (/api/v1) Alta Alta Baja Muy alta
Cabecera Baja Media Media Baja
Media type Baja Baja Alta Baja

CicloUrbana usa /api/v1. El razonamiento: la ventaja teórica de las otras dos no compensa el coste operativo. Con la versión en la ruta, un operario que mira los logs de Ribalta ve al instante qué versión usa cada cliente, un desarrollador nuevo entiende el esquema en tres segundos, y el día que llegue la v2 podremos desplegar /api/v2 en otra instancia y migrar clientes gradualmente. Es la elección de GitHub, Stripe y prácticamente toda API pública de gran escala.

Una advertencia sobre el número: la versión de la API no es la versión del software. Podemos publicar CicloUrbana 3.7.2 y seguir sirviendo /api/v1. La versión de la API solo cambia cuando el contrato rompe hacia atrás, y eso debería ocurrir muy pocas veces en la vida de un producto.

  1. REST frente a SOAP, GraphQL y gRPC

REST no es la única opción. Conocer las alternativas ayuda a saber cuándo REST no es la respuesta.

Criterio REST/HTTP SOAP GraphQL gRPC
Formato JSON (libre) XML obligatorio JSON con lenguaje de consulta Protobuf binario
Contrato OpenAPI (opcional) WSDL (obligatorio) Esquema (obligatorio) .proto (obligatorio)
Transporte HTTP HTTP, JMS, SMTP HTTP (normalmente un solo POST) HTTP/2
Caché HTTP Nativa No Difícil (todo es POST) No
Sobre-obtención de datos Frecuente Frecuente Resuelta por diseño Controlada
Legible por humanos Sí A duras penas Sí No (binario)
Rendimiento Bueno Bajo Bueno Muy alto
Streaming bidireccional No No Suscripciones Sí, nativo
Curva de aprendizaje Baja Alta Media Media

Cuándo elegir cada uno, en una frase:

  • REST: API pública, muchos clientes heterogéneos, caché importante, integraciones de terceros. Es el caso de CicloUrbana.
  • SOAP: integración con sistemas corporativos o de administración pública que ya lo exigen. Se elige por obligación, no por gusto.
  • GraphQL: clientes muy diversos con necesidades de datos muy distintas —una app móvil que quiere poco y un panel de control que quiere todo— donde la sobre-obtención es un problema real.
  • gRPC: comunicación entre microservicios internos, donde el rendimiento importa y ambos extremos los controlas tú. Volveremos a este escenario en 07-06.

No son excluyentes. Una arquitectura madura puede exponer REST al exterior y usar gRPC entre servicios internos. En el módulo 7, cuando CicloUrbana se divida en servicios, veremos ese contraste en vivo.

  1. Dónde encaja Spring MVC: el DispatcherServlet

Toda la teoría anterior se traduce, en Spring Boot, en un único componente central. Cuando añadimos spring-boot-starter-web en el módulo 1, la autoconfiguración —que ahora sabemos leer— registró un DispatcherServlet mapeado en /. Es el controlador frontal: todas las peticiones pasan por él.

sequenceDiagram
    participant C as Cliente (app de Ribalta)
    participant T as Tomcat + Filtros
    participant D as DispatcherServlet
    participant HM as HandlerMapping
    participant HA as HandlerAdapter
    participant CT as EstacionController
    participant MC as HttpMessageConverter (Jackson)

    C->>T: GET /api/v1/estaciones/1<br/>Accept: application/json
    T->>D: cadena de filtros y servlet
    D->>HM: ¿qué método atiende esta ruta?
    HM-->>D: EstacionController#obtenerPorId
    D->>HA: invócalo
    HA->>HA: resuelve argumentos (@PathVariable id = 1)
    HA->>CT: obtenerPorId(1L)
    CT-->>HA: Estacion
    HA->>MC: serializa según Accept
    MC-->>D: {"id":1,"nombre":"Plaza Mayor",...}
    D-->>T: 200 OK + Content-Type: application/json
    T-->>C: respuesta HTTP

Las piezas y su papel:

Componente Responsabilidad
DispatcherServlet Orquesta todo el flujo; es el punto de entrada único
HandlerMapping Decide qué método de qué controlador atiende la ruta (RequestMappingHandlerMapping lee las @GetMapping)
HandlerAdapter Invoca el método resolviendo sus argumentos
HandlerMethodArgumentResolver Convierte partes de la petición en parámetros Java (@PathVariable, @RequestParam, @RequestBody)
HttpMessageConverter Convierte entre el cuerpo HTTP y objetos Java; MappingJackson2HttpMessageConverter hace el JSON
HandlerExceptionResolver Traduce excepciones en respuestas HTTP (tema central de 03-06)

Merece la pena fijar una idea: un controlador de Spring no ve HTTP. Recibe un Long y devuelve un Estacion. Toda la traducción —parsear la ruta, elegir el formato, serializar, poner cabeceras— la hacen los componentes de la tabla. Por eso los controladores de Spring son tan compactos y tan fáciles de probar. Cuando algo no funciona como esperas, casi siempre el culpable es uno de esos componentes intermedios, y saber que existen es la mitad de la depuración.

Errores Comunes y Consejos

Poner verbos en la URL. POST /api/v1/estaciones/crear delata un diseño de nivel 1. El verbo ya está en el método HTTP; repetirlo en la ruta es redundante y bloquea la caché y los reintentos automáticos.

Devolver siempre 200. Un 200 OK con {"error": "estación no encontrada"} obliga a cada cliente a inspeccionar el cuerpo y engaña a monitorización, proxies y navegadores. El código de estado es la primera línea de la respuesta por algo.

Confundir 401 y 403. 401 = no sé quién eres. 403 = sé quién eres y no puedes. Se afianza en el módulo 5.

Devolver 500 por culpa del cliente. Si llega un JSON mal formado, es un 400. Un 5xx significa "hemos fallado nosotros" y contamina las alertas de producción.

Versionar demasiado pronto o demasiado tarde. Publicar /api/v2 porque se añade un campo opcional multiplica el coste de mantenimiento sin motivo: añadir campos es compatible. Al contrario, renombrar un campo en /api/v1 sin avisar rompe clientes en producción. La tabla del apartado 10 es la referencia.

Pluralizar a medias. /api/v1/estaciones y /api/v1/bicicleta/1 en la misma API. La incoherencia obliga a consultar la documentación para cada endpoint. Elige una convención y no la rompas nunca.

Consejo: escribe el contrato antes que el código. La tabla del apartado 6 se escribió antes que ningún controlador. Discutir una tabla cuesta minutos; renegociar una API desplegada cuesta semanas.

Consejo: piensa en el cliente cuando dudes. ¿/estaciones/1/bicicletas o /bicicletas?estacion=1? Pregúntate cuál escribirá con más naturalidad quien desarrolle la app. Si ambas son útiles, expón las dos: no es un pecado.

Ejercicios

Ejercicio 1: Diagnosticar el nivel de madurez

El ayuntamiento de Ribalta entrega la API de su sistema anterior de bicicletas. Estas son cuatro llamadas reales:

POST /bicing/api HTTP/1.1
Content-Type: application/json
{ "accion": "listarEstaciones", "ciudad": "ribalta" }

POST /bicing/api HTTP/1.1
{ "accion": "borrarEstacion", "id": 3 }

POST /bicing/api HTTP/1.1
{ "accion": "obtenerEstacion", "id": 999 }
→ 200 OK  { "ok": false, "mensaje": "no existe" }

Determina el nivel de Richardson, enumera qué restricciones REST incumple y reescribe las tres llamadas como una API de nivel 2 con sus códigos de estado.

Ejercicio 2: Diseñar los recursos de incidencias

CicloUrbana necesita gestionar incidencias: un ciudadano informa de que la bicicleta RB-0142 tiene la rueda pinchada; un operario la revisa y la cierra. Los datos de una incidencia son: identificador, matrícula de la bicicleta, descripción, fecha de apertura, estado (ABIERTA, EN_REVISION, CERRADA) y operario asignado.

Diseña el contrato: URLs, verbos, códigos de estado de éxito y de error, y justifica cómo modelas la transición de estado "cerrar una incidencia".

Ejercicio 3: Decidir la política de versionado

El equipo de CicloUrbana propone cuatro cambios para la próxima entrega. Para cada uno, decide si es compatible o incompatible y qué hacer:

  1. Añadir el campo bicicletasElectricasDisponibles a la respuesta de GET /api/v1/estaciones.
  2. Renombrar capacidad como capacidadTotal en todas las respuestas de estación.
  3. Cambiar latitud y longitud de dos campos sueltos a un objeto anidado {"ubicacion": {"lat":..., "lon":...}}.
  4. Aceptar un nuevo parámetro opcional ?ordenarPor=nombre en el listado de estaciones.

Soluciones

Solución 1.

Nivel: 0 (el pantano del POX). Hay una sola URL (/bicing/api), un solo verbo (POST) y la operación viaja en el cuerpo. HTTP se usa como mero túnel.

Restricciones incumplidas:

  • Interfaz uniforme / identificación de recursos: la estación 3 no tiene URI. No se puede enlazar, ni marcar, ni cachear individualmente.
  • Interfaz uniforme / mensajes autodescriptivos: 200 OK para un recurso inexistente miente. Ningún intermediario puede interpretar la respuesta sin conocer el formato propietario.
  • Cacheable: listar estaciones es una consulta, pero al ir por POST ninguna caché puede almacenarla. Todo el tráfico de lectura llega al servidor.

Reescritura a nivel 2:

GET /api/v1/estaciones?ciudad=ribalta HTTP/1.1
Accept: application/json
→ 200 OK, cuerpo: [ {...}, {...} ]
→ 200 OK con array vacío si no hay ninguna (no es un error)

DELETE /api/v1/estaciones/3 HTTP/1.1
→ 204 No Content   (sin cuerpo)
→ 404 Not Found    si la estación 3 no existía
→ 409 Conflict     si tiene alquileres activos

GET /api/v1/estaciones/999 HTTP/1.1
→ 404 Not Found
Content-Type: application/problem+json
{ "type": "https://api.ciclourbana.es/errores/recurso-no-encontrado",
  "title": "Estación no encontrada", "status": 404, "detail": "No existe la estación 999" }

El formato del error es RFC 7807, que se implementa en la lección 03-06. Nótese que el listado vacío no es un 404: la colección /estaciones existe aunque esté vacía. Un 404 en un listado solo procede si la ruta misma no existe.

Solución 2.

Las incidencias son un recurso de primer nivel: tienen identidad, ciclo de vida y se consultan por sí mismas.

Verbo Ruta Descripción Éxito Errores
GET /api/v1/incidencias Listado, filtrable por ?estado=ABIERTA&bicicleta=RB-0142 200 —
GET /api/v1/incidencias/{id} Detalle 200 404
POST /api/v1/incidencias Abrir incidencia 201 + Location 400, 404 (bicicleta inexistente)
PATCH /api/v1/incidencias/{id} Modificar descripción o asignar operario 200 400, 404
DELETE /api/v1/incidencias/{id} Eliminar (solo administración) 204 404, 409
GET /api/v1/bicicletas/{id}/incidencias Incidencias de una bicicleta concreta 200 404

Sobre la fecha de apertura y el estado inicial: no se aceptan en el POST. La fecha la pone el servidor con el bean Clock de ConfiguracionComun y el estado inicial es siempre ABIERTA. Aceptar del cliente datos que el servidor controla es una vía de manipulación.

Sobre cerrar una incidencia, hay dos diseños defendibles:

# Opción A: transición de estado como subrecurso de acción
POST /api/v1/incidencias/7/cerrar
{ "operarioId": 12, "resolucion": "Cámara sustituida" }
→ 200 OK

# Opción B: modificar el campo estado
PATCH /api/v1/incidencias/7
{ "estado": "CERRADA", "resolucion": "Cámara sustituida" }
→ 200 OK

La opción B es más pura —solo se modifica el estado del recurso— pero deja al servidor la tarea de detectar que ese PATCH concreto dispara efectos secundarios (notificar al ciudadano, registrar auditoría, devolver la bicicleta al servicio) y no valida bien las transiciones ilegales. La opción A nombra explícitamente una transición de la máquina de estados, es autodocumentada, permite exigir campos distintos para cada transición y se autoriza por separado en el módulo 5.

Recomendación: opción A, la misma que usará CicloUrbana en POST /api/v1/alquileres/{id}/finalizar. La regla general: si una modificación tiene un nombre en el lenguaje del negocio —"cerrar", "finalizar", "cancelar"— y dispara efectos más allá de cambiar un campo, merece su propio endpoint de acción.

Solución 3.

# Cambio Veredicto Acción
1 Añadir bicicletasElectricasDisponibles Compatible Se añade a /api/v1. Un cliente antiguo lo ignora; Jackson descarta por defecto los campos desconocidos
2 Renombrar capacidad a capacidadTotal Incompatible Rompe a todo cliente que lea capacidad
3 Anidar coordenadas en ubicacion Incompatible Cambia la estructura; los clientes fallan al leer latitud
4 Parámetro opcional ?ordenarPor=nombre Compatible Se añade a /api/v1 con un valor por defecto que preserva el orden actual

Estrategia para los cambios 2 y 3. Publicar /api/v2 por un renombrado es desproporcionado. El patrón correcto es la transición gradual con campos duplicados:

  1. En /api/v1, añadir capacidadTotal y ubicacion manteniendo capacidad, latitud y longitud. Los tres nuevos y los tres antiguos conviven; esto es compatible.
  2. Marcar los antiguos como obsoletos en la documentación OpenAPI (@Schema(deprecated = true), lección 03-07) y anunciar la fecha de retirada.
  3. Instrumentar con Actuator (módulo 7) qué clientes siguen leyendo los campos antiguos.
  4. Cuando el uso llegue a cero o venza el plazo, retirarlos en /api/v2.

La lección de fondo: la mayoría de los cambios "incompatibles" pueden convertirse en compatibles si se aceptan unos meses de duplicidad. Es casi siempre más barato que mantener dos versiones completas de la API en paralelo. La versión mayor se reserva para rediseños de verdad, no para cambios de nomenclatura.

Conclusión

Ya tenemos el marco. Sabes que REST es un estilo arquitectónico con seis restricciones —cliente-servidor, sin estado, cacheable, interfaz uniforme, sistema por capas y código bajo demanda— y, más importante, qué obliga cada una en el código que vas a escribir: nada de sesión en memoria, Cache-Control explícito, la lógica de negocio siempre en el servidor. Conoces el modelo de madurez de Richardson y por qué CicloUrbana se instala deliberadamente en el nivel 2. Has separado recurso, identificador y representación, la distinción que justificará los DTOs de la lección 03-05. Tienes las reglas de diseño de URLs del proyecto, la tabla completa de los trece endpoints que construiremos y el significado exacto de cada verbo en términos de seguridad e idempotencia, con el ejemplo del móvil en el túnel del metro de Ribalta para no olvidar por qué importa. Manejas los códigos de estado por familias y las dos confusiones clásicas —401 frente a 403, 400 frente a 422—. Sabes negociar contenido con Accept y Content-Type, y por qué 406 y 415 no son lo mismo. Has comparado las tres estrategias de versionado y entiendes por qué el curso elige /api/v1 pese a no ser la más pura. Y has situado REST frente a SOAP, GraphQL y gRPC para saber cuándo no es la respuesta.

Por último, has visto el recorrido completo de una petición dentro de Spring MVC: Tomcat, filtros, DispatcherServlet, HandlerMapping, HandlerAdapter, resolutores de argumentos, HttpMessageConverter. Esa secuencia es el mapa que usaremos para depurar durante todo el módulo, porque cuando una petición no llega al método esperado, o el JSON no sale como creías, el culpable siempre es una de esas piezas.

La lección 03-02, Creando Controladores REST, baja al código. Desmontaremos @RestController, veremos cómo se mapean rutas y cómo Spring extrae variables de plantilla, parámetros de consulta, cabeceras y cuerpos para convertirlos en argumentos Java; cómo Jackson transforma un record en JSON y cómo controlar esa transformación campo a campo y globalmente desde el application.yml; y cuándo devolver el objeto directamente y cuándo envolverlo en un ResponseEntity. Al final tendremos un EstacionController de verdad, con listado filtrado, paginación simple y consulta por identificador, probado con curl y con un fichero .http. Aquel GET /api/v1/estaciones que devolvía una lista fija empieza por fin a comportarse como una API.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados