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
- Qué es REST y de dónde viene
- Las seis restricciones de REST y lo que implican en la práctica
- Recursos, identificadores y representaciones
- El modelo de madurez de Richardson
- Diseño de las URLs de CicloUrbana
- El contrato completo de la API del módulo
- Verbos HTTP: seguridad e idempotencia
- Códigos de estado HTTP
- Negociación de contenido
- Versionado de la API
- REST frente a SOAP, GraphQL y gRPC
- Dónde encaja Spring MVC: el
DispatcherServlet - Errores Comunes y Consejos
- Ejercicios
- 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.
- 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á.
- 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.
- 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:
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 FoundAhora 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.
- 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.
- 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.
- 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
GETde 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.
- 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:
Unauthorizedes 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.
- 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 unPOSTo 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:
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:
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.
- 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.
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.
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.
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.
- 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.
- Dónde encaja Spring MVC: el
DispatcherServlet
DispatcherServletToda 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:
- Añadir el campo
bicicletasElectricasDisponiblesa la respuesta deGET /api/v1/estaciones. - Renombrar
capacidadcomocapacidadTotalen todas las respuestas de estación. - Cambiar
latitudylongitudde dos campos sueltos a un objeto anidado{"ubicacion": {"lat":..., "lon":...}}. - Aceptar un nuevo parámetro opcional
?ordenarPor=nombreen 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 OKpara 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
POSTninguna 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 OKLa 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:
- En
/api/v1, añadircapacidadTotalyubicacionmanteniendocapacidad,latitudylongitud. Los tres nuevos y los tres antiguos conviven; esto es compatible. - Marcar los antiguos como obsoletos en la documentación OpenAPI (
@Schema(deprecated = true), lección 03-07) y anunciar la fecha de retirada. - Instrumentar con Actuator (módulo 7) qué clientes siguen leyendo los campos antiguos.
- 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
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
