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
- Què és REST i d'on ve
- Les sis restriccions de REST i el que impliquen a la pràctica
- Recursos, identificadors i representacions
- El model de maduresa de Richardson
- Disseny de les URL de CicloUrbana
- El contracte complet de l'API del mòdul
- Verbs HTTP: seguretat i idempotència
- Codis d'estat HTTP
- Negociació de contingut
- Versionatge de l'API
- REST davant de SOAP, GraphQL i gRPC
- On encaixa Spring MVC: el
DispatcherServlet - Errors Comuns i Consells
- Exercicis
- 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.
- 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à.
- 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.
- 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:
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 FoundAra 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.
- 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.
- 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.
- 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
GETd'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.
- 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.
- 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 unPOSTo 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:
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:
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.
- 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.
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.
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.
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.
- 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.
- On encaixa Spring MVC: el
DispatcherServlet
DispatcherServletTota 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:
- Afegir el camp
bicicletesElectriquesDisponiblesa la resposta deGET /api/v1/estacions. - Reanomenar
capacitatcom acapacitatTotala totes les respostes d'estació. - Canviar
latitudilongitudde dos camps solts a un objecte imbricat{"ubicacio": {"lat":..., "lon":...}}. - Acceptar un nou paràmetre opcional
?ordenarPer=nomal 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 OKper 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
POSTcap 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 OKL'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:
- A
/api/v1, afegircapacitatTotaliubicaciomantenintcapacitat,latitudilongitud. Els tres nous i els tres antics conviuen; això és compatible. - Marcar els antics com a obsolets a la documentació OpenAPI (
@Schema(deprecated = true), lliçó 03-07) i anunciar la data de retirada. - Instrumentar amb Actuator (mòdul 7) quins clients continuen llegint els camps antics.
- 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
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
