Vam tancar el mòdul anterior amb el contenidor de Spring completament desmuntat: sabem com neixen els beans, com s'injecten, d'on surten les seves propietats i per què apareixen sols. Tot això passa per sota, on ningú no ho veu. A partir d'ara treballem a la capa que sí que es veu: l'API HTTP que consumiran l'aplicació mòbil de CicloUrbana, el tauler de control dels operaris de Ribalta i, eventualment, el portal de dades obertes de l'ajuntament. Aquesta primera lliçó no escriu controladors —això comença a la següent—, sinó que estableix el marc: què significa REST realment, com es dissenyen els recursos i les URL, quin verb i quin codi d'estat corresponen a cada operació, com es negocia el format de la resposta i com es versiona un contracte públic. Les decisions que prenguem aquí condicionen les sis lliçons següents i, en un sistema real, són molt cares de revertir: una URL publicada és una promesa.

Contingut

  1. Què és REST i d'on ve
  2. Les sis restriccions de REST i el que impliquen a la pràctica
  3. Recursos, identificadors i representacions
  4. El model de maduresa de Richardson
  5. Disseny de les URL de CicloUrbana
  6. El contracte complet de l'API del mòdul
  7. Verbs HTTP: seguretat i idempotència
  8. Codis d'estat HTTP
  9. Negociació de contingut
  10. Versionatge de l'API
  11. REST davant de SOAP, GraphQL i gRPC
  12. On encaixa Spring MVC: el DispatcherServlet
  13. Errors Comuns i Consells
  14. Exercicis

  1. Què és REST i d'on ve

REST —REpresentational State Transfer— no és un protocol, ni una llibreria, ni un format. És un estil arquitectònic descrit per Roy Fielding al capítol 5 de la seva tesi doctoral del 2000, Architectural Styles and the Design of Network-based Software Architectures. Fielding no va inventar REST observant com es feien les API de la seva època: el va descriure analitzant per què el web havia funcionat a una escala que cap sistema distribuït anterior no havia assolit.

Aquest origen explica moltes coses. REST no és "JSON sobre HTTP amb URL boniques". És un conjunt de restriccions que, aplicades a un sistema distribuït, li confereixen certes propietats desitjables: escalabilitat, independència entre client i servidor, tolerància a l'evolució i capacitat d'intercalar intermediaris (memòries cau, proxies, balancejadors) que entenen el trànsit sense conèixer l'aplicació.

La distinció important per a un desenvolupador és aquesta:

Afirmació Certa?
REST exigeix JSON No. REST no esmenta cap format. JSON és l'elecció habitual, no un requisit.
REST exigeix HTTP No formalment, però HTTP és l'única implementació rellevant i la que assumirem sempre.
REST exigeix URL amb substantius No literalment, però és la conseqüència natural de la restricció d'interfície uniforme.
Una API amb URL netes i JSON ja és REST No necessàriament. Sol quedar-se al nivell 2 de maduresa (apartat 4).
REST és més ràpid que SOAP No per definició. És més simple i més emmagatzemable a la memòria cau, cosa que sovint es tradueix en rapidesa.

A la pràctica del sector, "API REST" s'utilitza com a sinònim d'"API HTTP orientada a recursos amb JSON". CicloUrbana serà exactament això, amb la consciència de quines restriccions complim i quines no.

  1. Les sis restriccions de REST i el que impliquen a la pràctica

Fielding defineix sis restriccions. Cinc són obligatòries i una és opcional. L'interessant no és memoritzar-les, sinó veure què obliga cadascuna en el codi que escriurem.

Client-servidor

Separació de responsabilitats: el client s'ocupa de la interfície d'usuari, el servidor de les dades i les regles de negoci. Es comuniquen només mitjançant el contracte de l'API.

Què implica a CicloUrbana: el backend no genera mai HTML ni sap si el client és una app Android o un tauler web. Si demà l'ajuntament vol un tòtem tàctil a cada estació, no es toca el servidor. Això també significa que la lògica de negoci (es pot llogar una bicicleta amb el 15% de bateria?) viu al servidor, no a l'app: un client pot estar desactualitzat o ser maliciós.

Sense estat (stateless)

Cada petició conté tota la informació necessària per ser processada. El servidor no guarda context de sessió entre peticions.

Què implica: res de HttpSession amb el carretó de l'usuari, el pas de l'assistent o l'identificador del lloguer en curs. Si el client necessita autenticar-se, envia la credencial —un token— a cada petició (mòdul 5). Si necessita paginar, envia el número de pàgina cada vegada.

El benefici és l'escalabilitat horitzontal: si CicloUrbana creix i desplegem tres instàncies darrere d'un balancejador, qualsevol pot atendre qualsevol petició. Amb estat en memòria caldria afinitat de sessió o una sessió replicada, i totes dues coses compliquen el desplegament (ho veurem en contenidoritzar a 07-04).

El cost és que les peticions són més grans i de vegades es repeteix feina (validar el token a cada crida). És un intercanvi gairebé sempre favorable.

Emmagatzemable a la memòria cau

Cada resposta ha d'indicar, explícitament o implícitament, si es pot desar a la memòria cau i durant quant de temps.

Què implica: GET /api/v1/estacions retorna dades que canvien poc —una estació nova al mes— i pot dur Cache-Control: public, max-age=300. En canvi GET /api/v1/estacions/1/bicicletes canvia cada minut i ha d'anar amb Cache-Control: no-store o un max-age molt curt. Aquesta decisió, presa bé, treu més càrrega al servidor que qualsevol optimització de codi (hi tornarem a 09-02).

Interfície uniforme

És la restricció central i la que més distingeix REST. Es descompon en quatre subrestriccions:

Subrestricció Significat A CicloUrbana
Identificació de recursos Cada cosa té el seu URI /api/v1/estacions/1 identifica la Plaça Major
Manipulació mitjançant representacions El client modifica enviant una representació, no invocant mètodes PUT amb el JSON complet de l'estació
Missatges autodescriptius Cada missatge duu la informació per interpretar-lo Content-Type: application/json, codis d'estat
HATEOAS La resposta inclou enllaços a les transicions possibles Opcional; vegeu el nivell 3 a l'apartat 4

Sistema per capes

El client no sap si parla amb el servidor final o amb un intermediari. Entre l'app i el nostre Tomcat hi pot haver una CDN, un balancejador, un API gateway i un proxy de seguretat, i no canvia res.

Què implica: no assumir mai l'IP del client sense mirar X-Forwarded-For, no confiar que el port que veu l'aplicació és el que va fer servir el client, i generar les URL absolutes amb cura (ho veurem amb la capçalera Location a 03-03).

Codi sota demanda (opcional)

El servidor pot enviar codi executable al client. És l'única restricció opcional i en API de dades pràcticament no s'utilitza. CicloUrbana no l'aplicarà.

  1. Recursos, identificadors i representacions

Tres conceptes que es confonen constantment i que convé separar amb precisió.

  • Recurs: qualsevol concepte del domini que mereixi ser anomenat. "L'estació de la Plaça Major", "les bicicletes disponibles a l'estació 3", "el lloguer número 4471". És una noció abstracta, no una fila d'una taula.
  • Identificador (URI): el nom estable del recurs. /api/v1/estacions/1.
  • Representació: una forma concreta de mostrar l'estat del recurs en un moment donat. El mateix recurs pot tenir diverses representacions: JSON, XML, CSV, una versió resumida i una altra de detallada.
graph LR
    R["Recurs<br/>«L'estació Plaça Major»"] -->|s'identifica amb| U["/api/v1/estacions/1"]
    R -->|es representa com| J["application/json<br/>{ id, nom, capacitat... }"]
    R -->|es representa com| C["text/csv<br/>1,Placa Major,24"]

La conseqüència pràctica és important: el recurs no és la classe Java. El nostre record Estacio és una representació interna; el que l'API exposa és una altra cosa, que pot ometre camps, afegir camps calculats o agregar dades de diverses fonts. Aquesta separació és justament el tema de la lliçó 03-05 (DTO).

Un exemple concret de CicloUrbana: el recurs "estació" a la vista de llistat inclourà bicicletesDisponibles, un número calculat que no és al record Estacio. I no inclourà les coordenades exactes de l'ancoratge de manteniment, que sí que són al model intern. Recurs i classe són coses diferents.

  1. El model de maduresa de Richardson

Leonard Richardson va proposar el 2008 una escala de quatre nivells per mesurar quant s'aproxima una API a REST. És l'eina més útil per diagnosticar una API existent.

Nivell Nom Què fa servir Exemple a CicloUrbana
0 L'aiguamoll del POX Una URL, un verb POST /api/servei amb {"operacio":"llistarEstacions"}
1 Recursos Diverses URL, un verb POST /api/estacions/llistar, POST /api/estacions/1/esborrar
2 Verbs HTTP URL + verbs + codis d'estat GET /api/v1/estacions, DELETE /api/v1/estacions/1 → 204
3 HATEOAS L'anterior + enllaços a les respostes L'estació retorna _links.bicicletes i _links.llogar

Vegem-los amb peticions concretes.

Nivell 0 — tot passa per un únic punt d'entrada i el verb HTTP no significa res:

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

{ "operacio": "obtenirEstacio", "parametres": { "id": 1 } }

La resposta serà 200 OK fins i tot si l'estació no existeix, amb un {"error": "no trobada"} al cos. HTTP es fa servir com a simple túnel. És el model de SOAP i de moltes API internes heretades.

Nivell 1 — hi ha recursos amb URL pròpia, però les operacions continuen sent verbs a la ruta:

POST /api/estacions/1/obtenir
POST /api/estacions/1/esborrar
POST /api/estacions/crear

Es guanya llegibilitat, però un intermediari continua sense poder desar res a la memòria cau ni saber quines peticions són segures.

Nivell 2 — el verb HTTP expressa l'operació i el codi d'estat expressa el resultat:

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

Ara una CDN sap que pot desar el GET a la memòria cau, un proxy sap que pot reintentar el DELETE sense efectes secundaris i un client genèric entén el resultat sense llegir el cos. Aquest és el nivell en què opera la immensa majoria de les API professionals, i el que implementarà CicloUrbana.

Nivell 3 — les respostes descriuen què es pot fer a continuació:

{
  "id": 1,
  "nom": "Plaça Major",
  "bicicletesDisponibles": 7,
  "_links": {
    "self":        { "href": "/api/v1/estacions/1" },
    "bicicletes":  { "href": "/api/v1/estacions/1/bicicletes" },
    "llogar":      { "href": "/api/v1/lloguers", "method": "POST" }
  }
}

La idea és potent: si l'estació és buida, l'enllaç llogar simplement no apareix i el client no necessita replicar aquesta regla de negoci. Només el nivell 3 compleix la restricció d'interfície uniforme completa, i només ell mereix el nom de REST segons Fielding.

Per què CicloUrbana es queda al nivell 2. El nivell 3 exigeix clients que naveguin enllaços en lloc de construir URL, i a la pràctica gairebé cap client no ho fa: les apps mòbils duen les rutes escrites al codi. El cost de mantenir els enllaços no es compensa. És una decisió conscient i informada, que és exactament el que s'espera d'un dissenyador d'API: conèixer el nivell 3, aplicar-lo quan aporta —fluxos amb màquina d'estats complexa— i no per dogma. Spring ofereix spring-boot-starter-hateoas si algun dia es necessita.

  1. Disseny de les URL de CicloUrbana

Les regles que seguirà el projecte, amb la seva justificació:

Substantius en plural, mai verbs. /estacions, no /obtenirEstacions ni /estacio. El plural és coherent: /estacions és la col·lecció i /estacions/1 un element seu. Fer servir el singular obliga a decidir cas per cas i produeix incoherències.

Minúscules i guions per separar paraules. /api/v1/tipus-dusuari, no /tipusDUsuari ni /tipus_dusuari. Els noms de domini no distingeixen majúscules però les rutes sí, i el guió és la convenció del web.

Jerarquia per expressar pertinença. /api/v1/estacions/1/bicicletes són les bicicletes de l'estació 1. La regla pràctica: no imbricar més de dos nivells. /estacions/1/bicicletes/42/lloguers/7 és il·legible; si necessites el lloguer 7, demana'l per la seva URL pròpia, /lloguers/7.

Sense extensions ni sufixos de format. Res de /estacions.json. El format es negocia amb capçaleres (apartat 9).

Sense barra final. /api/v1/estacions i /api/v1/estacions/ han de ser la mateixa cosa; triem la primera forma.

Els filtres van a la cadena de consulta, no a la ruta. /api/v1/estacions?capacitatMinima=20&ciutat=ribalta, no /api/v1/estacions/capacitat-minima/20. La raó: la ruta identifica quin recurs; els paràmetres modifiquen com es retorna.

Bé Malament Per què
GET /api/v1/estacions GET /api/v1/getEstacions El verb ja és al mètode HTTP
GET /api/v1/estacions/1 GET /api/v1/estacio?id=1 Un element té URI pròpia
GET /api/v1/estacions?activa=true GET /api/v1/estacions/actives actives no és un recurs, és un filtre
GET /api/v1/estacions/1/bicicletes GET /api/v1/bicicletes?estacio=1 Totes dues valen; la primera expressa millor la pertinença
DELETE /api/v1/estacions/1 POST /api/v1/estacions/1/eliminar El verb HTTP és l'operació

Sobre el prefix /api: separa l'API de qualsevol contingut estàtic o pàgina web que l'aplicació pugui servir al mateix domini, i facilita les regles del proxy invers i de CORS. És una convenció tan estesa que la seva absència sorprèn.

  1. El contracte complet de l'API del mòdul

Aquest és l'objectiu del mòdul 3. En acabar la lliçó 03-07, CicloUrbana exposarà exactament això:

Verb Ruta Descripció Èxit Lliçó
GET /api/v1/estacions Llistat amb filtres i paginació 200 03-02
GET /api/v1/estacions/{id} Detall d'una estació 200 03-02
POST /api/v1/estacions Alta d'estació 201 + Location 03-03
PUT /api/v1/estacions/{id} Reemplaçament total 200 03-03
PATCH /api/v1/estacions/{id} Actualització parcial 200 03-03
DELETE /api/v1/estacions/{id} Baixa d'estació 204 03-03
GET /api/v1/estacions/{id}/bicicletes Bicicletes ancorades a l'estació 200 03-03
GET /api/v1/bicicletes Llistat de bicicletes de la xarxa 200 03-03
GET /api/v1/bicicletes/{id} Detall d'una bicicleta 200 03-03
POST /api/v1/bicicletes Alta de bicicleta 201 + Location 03-03
POST /api/v1/lloguers Iniciar un lloguer 201 + Location 03-03
POST /api/v1/lloguers/{id}/finalitzar Finalitzar un lloguer 200 03-03
GET /api/v1/lloguers/{id} Detall d'un lloguer 200 03-03

Tretze endpoints. Cap no duu un verb a la ruta llevat de finalitzar, que és una transició d'estat i no una operació CRUD; l'apartat corresponent de 03-03 justifica aquesta excepció amb detall.

Fixa't en una cosa que es farà evident a 03-05: GET /api/v1/estacions i GET /api/v1/estacions/{id} retornen representacions diferents del mateix recurs. El llistat retorna un resum; el detall inclou a més la llista de bicicletes ancorades. Això és legítim i molt comú: un mateix recurs, dues representacions amb diferent nivell de detall.

  1. Verbs HTTP: seguretat i idempotència

Dues propietats definides a l'RFC 9110 governen què pot fer un intermediari amb cada verb.

  • Segur (safe): no modifica l'estat del servidor. Un cercador pot recórrer tots els GET d'una API sense trencar res.
  • Idempotent: executar-lo N vegades deixa el sistema en el mateix estat que executar-lo una vegada. Això permet reintentar sense por quan la xarxa falla.
Verb Segur Idempotent Cos a la petició Ús a CicloUrbana
GET Sí Sí No Consultar estacions, bicicletes, lloguers
HEAD Sí Sí No Comprovar existència sense descarregar el cos
OPTIONS Sí Sí No Descobrir verbs permesos; utilitzat per CORS
POST No No Sí Crear estació, iniciar lloguer
PUT No Sí Sí Reemplaçar una estació completa
PATCH No No garantida Sí Modificar camps solts d'una estació
DELETE No Sí Opcional Donar de baixa una estació

La idempotència és més pràctica del que sembla. Imagina l'app de CicloUrbana al metro de Ribalta: l'usuari prem "llogar", el POST surt, el servidor el processa, i la resposta es perd perquè el mòbil entra en un túnel. L'app reintenta. Si el POST no és idempotent, el ciutadà acaba amb dos lloguers i dos cobraments.

Que POST no sigui idempotent és una característica, no un defecte: cada POST /api/v1/lloguers crea un lloguer nou, i això és el correcte. Per evitar duplicats per reintent existeix el patró de clau d'idempotència —una capçalera Idempotency-Key amb un UUID generat pel client— que el servidor recorda per no processar dues vegades la mateixa petició. És el mecanisme que fan servir les passarel·les de pagament. A CicloUrbana l'esmentem aquí i no l'implementem: exigeix emmagatzematge persistent, que arriba al mòdul 4.

Que DELETE sigui idempotent té una conseqüència de disseny concreta: DELETE /api/v1/estacions/1 sobre una estació ja esborrada hauria de retornar 204 o 404, però l'estat final és el mateix —l'estació no existeix—, i això és el que defineix la idempotència. Hi tornarem a 03-03.

PATCH no és idempotent en general perquè el seu cos pot descriure una operació relativa: "incrementa la capacitat en 4" dona un resultat diferent cada vegada. Si el cos descriu valors absoluts —"la capacitat passa a ser 28"— sí que ho és. És responsabilitat de qui dissenya l'API decidir-ho i documentar-ho.

  1. Codis d'estat HTTP

El codi d'estat és la part més ignorada i més valuosa d'una resposta HTTP. Retornar 200 OK amb {"exit": false} a dins obliga tot client a llegir el cos per saber si alguna cosa ha funcionat, i trenca qualsevol intermediari.

Les cinc famílies:

Família Significat Qui té el problema
1xx Informatiu Ningú; poc utilitzat
2xx Èxit Ningú
3xx Redirecció El client ha d'anar a un altre lloc
4xx Error del client El client: petició mal formada, no autoritzada, recurs inexistent
5xx Error del servidor Nosaltres: bug, base de dades caiguda, dependència no disponible

La distinció 4xx/5xx no és cosmètica. Els sistemes de monitoratge (mòdul 9) alerten sobre els 5xx i no sobre els 4xx, perquè un 404 és funcionament normal i un 500 és una trucada a les tres de la matinada. Retornar 500 quan el client ha enviat un JSON no vàlid genera soroll i desgasta la confiança en les alertes.

Els codis que farà servir CicloUrbana:

Codi Nom Quan el retorna CicloUrbana
200 OK Consulta correcta, PUT/PATCH que retorna el recurs actualitzat
201 Created Estació, bicicleta o lloguer creats; sempre amb capçalera Location
204 No Content DELETE correcte; PUT que no retorna cos
304 Not Modified Resposta a un GET condicional amb ETag coincident (03-03)
400 Bad Request JSON mal format o validació fallida (03-04)
401 Unauthorized Falta el token o no és vàlid (mòdul 5)
403 Forbidden Autenticat però sense permís (mòdul 5)
404 Not Found L'estació 999 no existeix
405 Method Not Allowed DELETE /api/v1/estacions sobre la col·lecció
409 Conflict Matrícula duplicada, estació plena en retornar una bicicleta
412 Precondition Failed If-Match amb un ETag obsolet (03-03)
415 Unsupported Media Type S'envia XML on s'espera JSON
422 Unprocessable Entity Sintaxi correcta però regla de negoci violada (03-04)
500 Internal Server Error Excepció no controlada; mai no ha de filtrar detalls (03-06)
503 Service Unavailable Dependència caiguda, arrencada en curs (mòdul 7)

Dues confusions freqüents que convé resoldre ja:

  • 401 davant de 403. 401 significa "no sé qui ets" (falta autenticació o no és vàlida); 403 significa "sé qui ets i no pots" (falta autorització). Els noms de l'estàndard són desafortunats: Unauthorized és en realitat no autenticat.
  • 400 davant de 422. 400 és "no entenc la teva petició" (JSON trencat, camp obligatori absent, tipus incorrecte); 422 és "t'entenc perfectament, però el que demanes no és acceptable" (vols llogar una bicicleta que és en manteniment). La lliçó 03-04 fixa la política del projecte.

  1. Negociació de contingut

HTTP permet que el mateix recurs se serveixi en diversos formats i que client i servidor acordin quin. El mecanisme és un parell de capçaleres:

  • Content-Type: descriu el format del cos que s'està enviant. El posa qui envia el cos, sigui el client en un POST o el servidor a la resposta.
  • Accept: és la llista de formats que el client sap interpretar, en ordre de preferència. El posa sempre el client.
POST /api/v1/estacions HTTP/1.1
Host: api.ciclourbana.ribalta.example
Content-Type: application/json
Accept: application/json

{ "nom": "Mercat Central", "capacitat": 20 }

Aquí el client diu: "t'envio JSON i vull JSON de tornada". El servidor respon:

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

Els tipus MIME rellevants per al curs:

Tipus MIME Ús
application/json El format per defecte de tota l'API de CicloUrbana
application/problem+json Respostes d'error amb RFC 7807 Problem Details (03-06)
application/x-www-form-urlencoded Formularis HTML clàssics; aquesta API no el fa servir
multipart/form-data Pujada de fitxers (fotos d'incidència a les bicicletes)
text/csv Exportació de dades per a l'ajuntament
application/vnd.ciclourbana.v2+json Versionatge per media type (apartat 10)

La capçalera Accept admet pesos de preferència:

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

El client prefereix JSON, accepta CSV i, en darrer cas, qualsevol cosa. Si el servidor no pot satisfer cap de les opcions, respon 406 Not Acceptable. Si el client envia un Content-Type que el servidor no sap llegir, la resposta és 415 Unsupported Media Type. Són dos codis simètrics que es confonen sovint: 406 mira Accept, 415 mira Content-Type.

Spring implementa tot això automàticament a partir dels atributs produces i consumes de @RequestMapping, que veurem a la lliçó següent.

  1. Versionatge de l'API

Una API pública és un contracte. Tan bon punt l'app mòbil de CicloUrbana estigui publicada a les botigues, hi haurà ciutadans de Ribalta amb versions antigues instal·lades durant mesos. Canviar el nom d'un camp els trenca els telèfons.

La regla bàsica és distingir canvis compatibles d'incompatibles:

Compatible (no exigeix versió nova) Incompatible (exigeix versió nova)
Afegir un camp opcional a una resposta Eliminar o reanomenar un camp
Afegir un endpoint nou Canviar el tipus d'un camp
Afegir un paràmetre de consulta opcional Fer obligatori un camp que no ho era
Afegir un valor a un enum de resposta Canviar el significat d'un camp
Relaxar una validació Canviar un codi d'estat d'èxit

Les tres estratègies de versionatge:

Per URL — /api/v1/estacions, /api/v2/estacions.

GET /api/v1/estacions HTTP/1.1

Avantatges: visible d'un cop d'ull, trivial de provar en un navegador o amb curl, fàcil d'enrutar en un proxy o de desplegar com a aplicacions separades, evident als logs. Inconvenient teòric: dues URL diferents per al mateix recurs, cosa que trenca la idea d'identificador únic.

Per capçalera personalitzada — el client envia X-API-Version: 1.

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

Avantatge: l'URI del recurs és una de sola. Inconvenients: invisible al log d'accés llevat de configuració extra, impossible de provar enganxant una URL al navegador, i la memòria cau HTTP necessita Vary: X-API-Version per no servir la versió equivocada.

Per media type (content negotiation de versió) — la variant més purista.

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

Avantatge: teòricament la més correcta; la versió forma part de la representació, no de la identitat. Inconvenients: la més difícil d'explicar a un equip client, la més incòmoda de provar i la que pitjor suporten les eines.

Estratègia Visibilitat Facilitat de prova Puresa REST Adopció real
URL (/api/v1) Alta Alta Baixa Molt alta
Capçalera Baixa Mitjana Mitjana Baixa
Media type Baixa Baixa Alta Baixa

CicloUrbana fa servir /api/v1. El raonament: l'avantatge teòric de les altres dues no compensa el cost operatiu. Amb la versió a la ruta, un operari que mira els logs de Ribalta veu a l'instant quina versió fa servir cada client, un desenvolupador nou entén l'esquema en tres segons, i el dia que arribi la v2 podrem desplegar /api/v2 en una altra instància i migrar clients gradualment. És l'elecció de GitHub, Stripe i pràcticament tota API pública de gran escala.

Un advertiment sobre el número: la versió de l'API no és la versió del programari. Podem publicar CicloUrbana 3.7.2 i continuar servint /api/v1. La versió de l'API només canvia quan el contracte trenca cap enrere, i això hauria de passar molt poques vegades en la vida d'un producte.

  1. REST davant de SOAP, GraphQL i gRPC

REST no és l'única opció. Conèixer les alternatives ajuda a saber quan REST no és la resposta.

Criteri REST/HTTP SOAP GraphQL gRPC
Format JSON (lliure) XML obligatori JSON amb llenguatge de consulta Protobuf binari
Contracte OpenAPI (opcional) WSDL (obligatori) Esquema (obligatori) .proto (obligatori)
Transport HTTP HTTP, JMS, SMTP HTTP (normalment un sol POST) HTTP/2
Memòria cau HTTP Nativa No Difícil (tot és POST) No
Sobreobtenció de dades Freqüent Freqüent Resolta per disseny Controlada
Llegible per humans Sí A males penes Sí No (binari)
Rendiment Bo Baix Bo Molt alt
Streaming bidireccional No No Subscripcions Sí, natiu
Corba d'aprenentatge Baixa Alta Mitjana Mitjana

Quan triar cadascun, en una frase:

  • REST: API pública, molts clients heterogenis, memòria cau important, integracions de tercers. És el cas de CicloUrbana.
  • SOAP: integració amb sistemes corporatius o d'administració pública que ja l'exigeixen. Es tria per obligació, no per gust.
  • GraphQL: clients molt diversos amb necessitats de dades molt diferents —una app mòbil que vol poc i un tauler de control que ho vol tot— on la sobreobtenció és un problema real.
  • gRPC: comunicació entre microserveis interns, on el rendiment importa i tots dos extrems els controles tu. Tornarem a aquest escenari a 07-06.

No són excloents. Una arquitectura madura pot exposar REST a l'exterior i fer servir gRPC entre serveis interns. Al mòdul 7, quan CicloUrbana es divideixi en serveis, veurem aquest contrast en directe.

  1. On encaixa Spring MVC: el DispatcherServlet

Tota la teoria anterior es tradueix, a Spring Boot, en un únic component central. Quan vam afegir spring-boot-starter-web al mòdul 1, l'autoconfiguració —que ara sabem llegir— va registrar un DispatcherServlet mapat a /. És el controlador frontal: totes les peticions hi passen.

sequenceDiagram
    participant C as Client (app de Ribalta)
    participant T as Tomcat + Filtres
    participant D as DispatcherServlet
    participant HM as HandlerMapping
    participant HA as HandlerAdapter
    participant CT as EstacioController
    participant MC as HttpMessageConverter (Jackson)

    C->>T: GET /api/v1/estacions/1<br/>Accept: application/json
    T->>D: cadena de filtres i servlet
    D->>HM: quin mètode atén aquesta ruta?
    HM-->>D: EstacioController#obtenirPerId
    D->>HA: invoca'l
    HA->>HA: resol arguments (@PathVariable id = 1)
    HA->>CT: obtenirPerId(1L)
    CT-->>HA: Estacio
    HA->>MC: serialitza segons Accept
    MC-->>D: {"id":1,"nom":"Placa Major",...}
    D-->>T: 200 OK + Content-Type: application/json
    T-->>C: resposta HTTP

Les peces i el seu paper:

Component Responsabilitat
DispatcherServlet Orquestra tot el flux; és el punt d'entrada únic
HandlerMapping Decideix quin mètode de quin controlador atén la ruta (RequestMappingHandlerMapping llegeix les @GetMapping)
HandlerAdapter Invoca el mètode resolent els seus arguments
HandlerMethodArgumentResolver Converteix parts de la petició en paràmetres Java (@PathVariable, @RequestParam, @RequestBody)
HttpMessageConverter Converteix entre el cos HTTP i objectes Java; MappingJackson2HttpMessageConverter fa el JSON
HandlerExceptionResolver Tradueix excepcions en respostes HTTP (tema central de 03-06)

Val la pena fixar una idea: un controlador de Spring no veu HTTP. Rep un Long i retorna una Estacio. Tota la traducció —analitzar la ruta, triar el format, serialitzar, posar capçaleres— la fan els components de la taula. Per això els controladors de Spring són tan compactes i tan fàcils de provar. Quan alguna cosa no funciona com esperes, gairebé sempre el culpable és un d'aquests components intermedis, i saber que existeixen és la meitat de la depuració.

Errors Comuns i Consells

Posar verbs a la URL. POST /api/v1/estacions/crear delata un disseny de nivell 1. El verb ja és al mètode HTTP; repetir-lo a la ruta és redundant i bloqueja la memòria cau i els reintents automàtics.

Retornar sempre 200. Un 200 OK amb {"error": "estació no trobada"} obliga cada client a inspeccionar el cos i enganya monitoratge, proxies i navegadors. El codi d'estat és la primera línia de la resposta per alguna cosa.

Confondre 401 i 403. 401 = no sé qui ets. 403 = sé qui ets i no pots. S'afiança al mòdul 5.

Retornar 500 per culpa del client. Si arriba un JSON mal format, és un 400. Un 5xx significa "hem fallat nosaltres" i contamina les alertes de producció.

Versionar massa aviat o massa tard. Publicar /api/v2 perquè s'afegeix un camp opcional multiplica el cost de manteniment sense motiu: afegir camps és compatible. Al contrari, reanomenar un camp a /api/v1 sense avisar trenca clients en producció. La taula de l'apartat 10 és la referència.

Pluralitzar a mitges. /api/v1/estacions i /api/v1/bicicleta/1 a la mateixa API. La incoherència obliga a consultar la documentació per a cada endpoint. Tria una convenció i no la trenquis mai.

Consell: escriu el contracte abans que el codi. La taula de l'apartat 6 es va escriure abans que cap controlador. Discutir una taula costa minuts; renegociar una API desplegada costa setmanes.

Consell: pensa en el client quan dubtis. /estacions/1/bicicletes o /bicicletes?estacio=1? Pregunta't quina escriurà amb més naturalitat qui desenvolupi l'app. Si totes dues són útils, exposa-les totes dues: no és cap pecat.

Exercicis

Exercici 1: Diagnosticar el nivell de maduresa

L'ajuntament de Ribalta lliura l'API del seu sistema anterior de bicicletes. Aquestes són quatre crides reals:

POST /bicing/api HTTP/1.1
Content-Type: application/json
{ "accio": "llistarEstacions", "ciutat": "ribalta" }

POST /bicing/api HTTP/1.1
{ "accio": "esborrarEstacio", "id": 3 }

POST /bicing/api HTTP/1.1
{ "accio": "obtenirEstacio", "id": 999 }
→ 200 OK  { "ok": false, "missatge": "no existeix" }

Determina el nivell de Richardson, enumera quines restriccions REST incompleix i reescriu les tres crides com una API de nivell 2 amb els seus codis d'estat.

Exercici 2: Dissenyar els recursos d'incidències

CicloUrbana necessita gestionar incidències: un ciutadà informa que la bicicleta RB-0142 té la roda punxada; un operari la revisa i la tanca. Les dades d'una incidència són: identificador, matrícula de la bicicleta, descripció, data d'obertura, estat (OBERTA, EN_REVISIO, TANCADA) i operari assignat.

Dissenya el contracte: URL, verbs, codis d'estat d'èxit i d'error, i justifica com modelitzes la transició d'estat "tancar una incidència".

Exercici 3: Decidir la política de versionatge

L'equip de CicloUrbana proposa quatre canvis per al lliurament següent. Per a cadascun, decideix si és compatible o incompatible i què cal fer:

  1. Afegir el camp bicicletesElectriquesDisponibles a la resposta de GET /api/v1/estacions.
  2. Reanomenar capacitat com a capacitatTotal a totes les respostes d'estació.
  3. Canviar latitud i longitud de dos camps solts a un objecte imbricat {"ubicacio": {"lat":..., "lon":...}}.
  4. Acceptar un nou paràmetre opcional ?ordenarPer=nom al llistat d'estacions.

Solucions

Solució 1.

Nivell: 0 (l'aiguamoll del POX). Hi ha una sola URL (/bicing/api), un sol verb (POST) i l'operació viatja al cos. HTTP es fa servir com a mer túnel.

Restriccions incomplertes:

  • Interfície uniforme / identificació de recursos: l'estació 3 no té URI. No es pot enllaçar, ni marcar, ni desar individualment a la memòria cau.
  • Interfície uniforme / missatges autodescriptius: 200 OK per a un recurs inexistent menteix. Cap intermediari no pot interpretar la resposta sense conèixer el format propietari.
  • Emmagatzemable a la memòria cau: llistar estacions és una consulta, però com que va per POST cap memòria cau no la pot desar. Tot el trànsit de lectura arriba al servidor.

Reescriptura a nivell 2:

GET /api/v1/estacions?ciutat=ribalta HTTP/1.1
Accept: application/json
→ 200 OK, cos: [ {...}, {...} ]
→ 200 OK amb array buit si no n'hi ha cap (no és un error)

DELETE /api/v1/estacions/3 HTTP/1.1
→ 204 No Content   (sense cos)
→ 404 Not Found    si l'estació 3 no existia
→ 409 Conflict     si té lloguers actius

GET /api/v1/estacions/999 HTTP/1.1
→ 404 Not Found
Content-Type: application/problem+json
{ "type": "https://api.ciclourbana.example/errors/recurs-no-trobat",
  "title": "Estació no trobada", "status": 404, "detail": "No existeix l'estació 999" }

El format de l'error és RFC 7807, que s'implementa a la lliçó 03-06. Observa que el llistat buit no és un 404: la col·lecció /estacions existeix encara que sigui buida. Un 404 en un llistat només escau si la ruta mateixa no existeix.

Solució 2.

Les incidències són un recurs de primer nivell: tenen identitat, cicle de vida i es consulten per si mateixes.

Verb Ruta Descripció Èxit Errors
GET /api/v1/incidencies Llistat, filtrable per ?estat=OBERTA&bicicleta=RB-0142 200 —
GET /api/v1/incidencies/{id} Detall 200 404
POST /api/v1/incidencies Obrir incidència 201 + Location 400, 404 (bicicleta inexistent)
PATCH /api/v1/incidencies/{id} Modificar descripció o assignar operari 200 400, 404
DELETE /api/v1/incidencies/{id} Eliminar (només administració) 204 404, 409
GET /api/v1/bicicletes/{id}/incidencies Incidències d'una bicicleta concreta 200 404

Sobre la data d'obertura i l'estat inicial: no s'accepten al POST. La data la posa el servidor amb el bean Clock de ConfiguracioComuna i l'estat inicial és sempre OBERTA. Acceptar del client dades que el servidor controla és una via de manipulació.

Sobre tancar una incidència, hi ha dos dissenys defensables:

# Opció A: transició d'estat com a subrecurs d'acció
POST /api/v1/incidencies/7/tancar
{ "operariId": 12, "resolucio": "Cambra substituida" }
→ 200 OK

# Opció B: modificar el camp estat
PATCH /api/v1/incidencies/7
{ "estat": "TANCADA", "resolucio": "Cambra substituida" }
→ 200 OK

L'opció B és més pura —només es modifica l'estat del recurs— però deixa al servidor la tasca de detectar que aquest PATCH concret dispara efectes secundaris (notificar el ciutadà, registrar auditoria, retornar la bicicleta al servei) i no valida bé les transicions il·legals. L'opció A anomena explícitament una transició de la màquina d'estats, és autodocumentada, permet exigir camps diferents per a cada transició i s'autoritza per separat al mòdul 5.

Recomanació: opció A, la mateixa que farà servir CicloUrbana a POST /api/v1/lloguers/{id}/finalitzar. La regla general: si una modificació té un nom en el llenguatge del negoci —"tancar", "finalitzar", "cancel·lar"— i dispara efectes més enllà de canviar un camp, mereix el seu propi endpoint d'acció.

Solució 3.

# Canvi Veredicte Acció
1 Afegir bicicletesElectriquesDisponibles Compatible S'afegeix a /api/v1. Un client antic l'ignora; Jackson descarta per defecte els camps desconeguts
2 Reanomenar capacitat a capacitatTotal Incompatible Trenca tot client que llegeixi capacitat
3 Imbricar coordenades a ubicacio Incompatible Canvia l'estructura; els clients fallen en llegir latitud
4 Paràmetre opcional ?ordenarPer=nom Compatible S'afegeix a /api/v1 amb un valor per defecte que preserva l'ordre actual

Estratègia per als canvis 2 i 3. Publicar /api/v2 per un canvi de nom és desproporcionat. El patró correcte és la transició gradual amb camps duplicats:

  1. A /api/v1, afegir capacitatTotal i ubicacio mantenint capacitat, latitud i longitud. Els tres nous i els tres antics conviuen; això és compatible.
  2. Marcar els antics com a obsolets a la documentació OpenAPI (@Schema(deprecated = true), lliçó 03-07) i anunciar la data de retirada.
  3. Instrumentar amb Actuator (mòdul 7) quins clients continuen llegint els camps antics.
  4. Quan l'ús arribi a zero o venci el termini, retirar-los a /api/v2.

La lliçó de fons: la majoria dels canvis "incompatibles" es poden convertir en compatibles si s'accepten uns mesos de duplicitat. És gairebé sempre més barat que mantenir dues versions completes de l'API en paral·lel. La versió major es reserva per a redissenys de veritat, no per a canvis de nomenclatura.

Conclusió

Ja tenim el marc. Saps que REST és un estil arquitectònic amb sis restriccions —client-servidor, sense estat, emmagatzemable a la memòria cau, interfície uniforme, sistema per capes i codi sota demanda— i, més important, què obliga cadascuna en el codi que escriuràs: res de sessió en memòria, Cache-Control explícit, la lògica de negoci sempre al servidor. Coneixes el model de maduresa de Richardson i per què CicloUrbana s'instal·la deliberadament al nivell 2. Has separat recurs, identificador i representació, la distinció que justificarà els DTO de la lliçó 03-05. Tens les regles de disseny d'URL del projecte, la taula completa dels tretze endpoints que construirem i el significat exacte de cada verb en termes de seguretat i idempotència, amb l'exemple del mòbil al túnel del metro de Ribalta per no oblidar per què importa. Manegues els codis d'estat per famílies i les dues confusions clàssiques —401 davant de 403, 400 davant de 422—. Saps negociar contingut amb Accept i Content-Type, i per què 406 i 415 no són el mateix. Has comparat les tres estratègies de versionatge i entens per què el curs tria /api/v1 tot i no ser la més pura. I has situat REST davant de SOAP, GraphQL i gRPC per saber quan no és la resposta.

Finalment, has vist el recorregut complet d'una petició dins de Spring MVC: Tomcat, filtres, DispatcherServlet, HandlerMapping, HandlerAdapter, resolutors d'arguments, HttpMessageConverter. Aquesta seqüència és el mapa que farem servir per depurar durant tot el mòdul, perquè quan una petició no arriba al mètode esperat, o el JSON no surt com et pensaves, el culpable sempre és una d'aquestes peces.

La lliçó 03-02, Creant Controladors REST, baixa al codi. Desmuntarem @RestController, veurem com es mapen rutes i com Spring extreu variables de plantilla, paràmetres de consulta, capçaleres i cossos per convertir-los en arguments Java; com Jackson transforma un record en JSON i com controlar aquesta transformació camp a camp i globalment des de l'application.yml; i quan retornar l'objecte directament i quan embolcallar-lo en un ResponseEntity. Al final tindrem un EstacioController de veritat, amb llistat filtrat, paginació simple i consulta per identificador, provat amb curl i amb un fitxer .http. Aquell GET /api/v1/estacions que retornava una llista fixa comença per fi a comportar-se com una API.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats