A la lliçó anterior vam fixar el mètode de treball i vam decidir, amb criteri, quins substantius del domini de la Botiga Aroma mereixen ser recursos. Ara toca el següent: donar-los adreça. La URI és la part més visible i més duradora d'una API —els clients l'escriuen al seu codi, la desen a marcadors, la copien a tiquets— i per això és també la més cara de canviar. Aquesta lliçó estableix les regles d'anomenament, resol quan cal imbricar i quan no, tria el tipus d'identificador i afronta el problema que cap CRUD no resol per si sol: com es modelen accions com pagar una comanda o moderar una ressenya. Acabarem amb el mapa complet d'URIs de la Botiga Aroma, que la resta del mòdul donarà per bo.
Contingut
- Què és un recurs i què és una URI
- Substantius i no verbs
- Col·leccions i elements
- Regles d'anomenament
- Jerarquia i imbricació
- Recursos singleton
- Path parameters davant de query parameters
- Disseny d'identificadors
- Accions que no són CRUD
- Mapa d'URIs de la Botiga Aroma
- Què és un recurs i què és una URI
Recordem la definició de 01-04: un recurs és qualsevol cosa amb identitat sobre la qual tingui sentit operar; la URI és el seu identificador estable; la representació és una de les seves possibles formes concretes (el JSON que viatja, el PDF, l'HTML).
Tres conseqüències pràctiques que governen tota aquesta lliçó:
- Un recurs pot tenir diverses representacions (
/comandes/com_5001/facturaen JSON o en PDF), però manté una sola URI. La representació es tria per negociació de contingut (02-05), no canviant la URL amb.pdf. - Una URI identifica, no descriu l'operació. Allò que es fa amb el recurs ho diu el mètode (02-03).
- Les URIs haurien de sobreviure als canvis interns. Si
/cafes/caf_001deixa de funcionar perquè s'ha refactoritzat la base de dades, hem trencat el contracte.
- Substantius i no verbs
La regla més citada del disseny REST, i la que més s'incompleix:
| ❌ Verb a la ruta | ✅ Substantiu + mètode |
|---|---|
GET /obtenirCafes |
GET /cafes |
POST /crearComanda |
POST /comandes |
POST /actualitzarEstoc?id=caf_001 |
PATCH /cafes/caf_001 |
GET /esborrarRessenya?id=res_101 |
DELETE /ressenyes/res_101 |
POST /llistarComandesDeClient |
GET /clients/cli_842/comandes |
Per què importa, més enllà de l'estètica:
- El mètode ja és el verb.
GET /obtenirCafesrepeteix el verb i, pitjor encara, permet la incoherènciaPOST /obtenirCafes. - Es perd la semàntica d'HTTP.
GET /esborrarRessenyaés una barbaritat funcional: un cercador o un prefetch del navegador podria esborrar ressenyes en recórrer enllaços, perquè GET és segur per definició (02-03). - Es perd la previsibilitat. Amb verbs, cada endpoint s'ha de memoritzar:
obtenirCafes,getComandes,llistarRessenyes,consultarClient.
Compte amb un matís important: els identificadors del codi sí que van en català i amb verb (obtenirCafes(), crearComanda()), com fixa la guia d'estil. El que no porta verb és la URI.
- Col·leccions i elements
Tota API orientada a recursos s'estructura sobre dues figures:
- Col·lecció: un conjunt de recursos del mateix tipus.
/cafes. - Element (o recurs individual): un membre concret.
/cafes/caf_001.
El patró es llegeix d'esquerra a dreta com una ruta de navegació:
/v1/clients/cli_842/comandes/com_5001 │ │ │ │ └── element: una comanda concreta │ │ │ └─────────── col·lecció: les comandes d'aquest client │ │ └─────────────────── element: un client concret │ └─────────────────────────── col·lecció: tots els clients └─────────────────────────────── versió de l'API
Cada nivell alterna col·lecció → element → col·lecció → element. Si la teva URI trenca aquesta alternança (/clients/comandes/cli_842), gairebé sempre hi ha un error de disseny.
I cada figura admet operacions diferents, cosa que anticipa 02-03:
Col·lecció /cafes |
Element /cafes/caf_001 |
|
|---|---|---|
GET |
Llista, filtrada i paginada | Retorna aquest cafè |
POST |
Crea'n un de nou | No es fa servir (llevat de subrecurs d'acció) |
PUT |
No es fa servir (substituir tota la col·lecció és perillós) | Substitueix aquest cafè |
PATCH |
No es fa servir | Modifica camps d'aquest cafè |
DELETE |
No es fa servir (esborrar tot el catàleg per accident) | Esborra aquest cafè |
- Regles d'anomenament
Aquestes són les regles que la Botiga Aroma incorpora a la seva guia d'estil.
4.1. Plural consistent
/cafes, /clients, /comandes, /ressenyes, /cistelles. Sempre plural, fins i tot quan soni estrany, perquè l'alternativa —singular per a l'element i plural per a la col·lecció— obliga a recordar dues formes per recurs.
L'única excepció són els singleton (secció 6), que per definició no són col·lecció.
4.2. Minúscules sempre
El component de ruta d'una URL distingeix majúscules de minúscules (a diferència del nom d'amfitrió). /Cafes i /cafes són dos recursos diferents per a l'estàndard, i acceptar tots dos duplica la superfície del contracte i espatlla la memòria cau.
4.3. Guions per a paraules compostes (kebab-case)
Motius: és la convenció dominant al web, és més llegible i els cercadors tracten el guionet com a separador de paraules (rellevant si part de l'API s'indexa o si comparteixes enllaços de documentació). Fixa't en l'asimetria deliberada: kebab-case a la URI, camelCase al JSON. És una inconsistència aparent, però són dos mons amb convencions pròpies i consolidades; l'important és que cada món sigui coherent amb si mateix.
4.4. Sense extensions de fitxer
El format és una qüestió de representació, i es negocia amb capçaleres (02-05). Posar .json barreja identitat i format: si demà hi afegeixes XML o PDF, cada recurs té tres URIs per a la mateixa cosa.
4.5. Sense barra final
/cafes i /cafes/ són tècnicament rutes diferents. Tria'n una —la Botiga Aroma fa servir sense barra final— i redirigeix l'altra amb 301 (02-04) en lloc de servir-les totes dues.
4.6. Sense sufixos tècnics ni argot intern
El nom de la classe, de la taula o del patró d'implementació no és cosa del consumidor. Compte també amb /api: si l'amfitrió ja és api.botigaaroma.example, repetir-ho a la ruta és redundant.
4.7. Sense caràcters problemàtics
Res d'accents, ç, punts volats, espais ni majúscules als segments de ruta que tu controles. Per això l'acció és /anullacio i no /anul·lació: encara que els navegadors moderns codifiquin els caràcters no ASCII, a la pràctica acaben apareixent com /anul%C2%B7laci%C3%B3 als logs, als exemples de curl i als clients antics.
- Jerarquia i imbricació
Imbricar expressa pertinença: /cafes/caf_001/ressenyes són les ressenyes d'aquest cafè.
5.1. La regla pràctica
Imbrica un subrecurs només si no té sentit fora del seu pare, o si la relació de pertinença és la manera natural d'accedir-hi.
Exemples a la Botiga Aroma:
| URI | Imbricar? | Raó |
|---|---|---|
/cafes/caf_001/ressenyes |
Sí | Les ressenyes d'un cafè són un cas d'ús central (la fitxa de producte) |
/cistelles/cis_77/linies/caf_002 |
Sí | Una línia de cistella no existeix sense la seva cistella |
/comandes/com_5001/pagament |
Sí | El pagament pertany a una comanda concreta |
/clients/cli_842/comandes |
Sí | "Les meves comandes" és un cas d'ús real de la SPA i d'Aroma Mòbil |
/clients/cli_842/comandes/com_5001/linies/1/cafe |
No | Quatre nivells: il·legible i fràgil. Enllaça al cafè, no l'imbriquis |
/origens/etiopia/cafes |
No | L'origen és un atribut: es resol amb un filtre ?origen=Etiòpia |
5.2. Màxim dos nivells
Una regla que estalvia disgustos: no passis de /colleccio/{id}/subcolleccio/{id}. A partir d'aquí la URI es torna il·legible, acobla el client a una jerarquia que pot canviar i obliga a validar cadenes de pertinença llargues.
Quan necessitis baixar més, talla la jerarquia i fes servir una col·lecció de primer nivell amb filtre:
# En lloc d'imbricar tres nivells
curl "https://api.botigaaroma.example/v1/ressenyes?cafeId=caf_001&estat=pendent_moderacio"5.3. Doble accés: imbricat i pla
Un mateix recurs pot ser accessible per dues rutes si cadascuna serveix a un cas d'ús diferent. A la Botiga Aroma:
# Fitxa de producte: les ressenyes d'un cafè (SPA i Aroma Mòbil)
GET /v1/cafes/caf_001/ressenyes
# Panell intern: totes les ressenyes pendents de moderar, de qualsevol cafè
GET /v1/ressenyes?estat=pendent_moderacioRegles perquè això no es converteixi en un problema:
- L'element viu en una sola URI canònica:
/ressenyes/res_101. La forma imbricada és només per llistar i crear. - L'enllaç
selfde la representació apunta sempre a la canònica, perquè dues rutes no generin dues identitats. - La creació imbricada és més còmoda:
POST /cafes/caf_001/ressenyesno necessita repetircafeIdal cos, perquè ja és a la URI.
graph TD
C["GET /v1/cafes/caf_001/ressenyes<br/><i>vista imbricada</i>"] --> R["res_101<br/>res_102"]
P["GET /v1/ressenyes?estat=pendent_moderacio<br/><i>vista plana amb filtre</i>"] --> R
R --> CAN["URI canònica de l'element:<br/><b>/v1/ressenyes/res_101</b><br/>(és la que va a _links.self)"]
- Recursos singleton
Un singleton és un recurs del qual només existeix una instància en el seu context. No té col·lecció ni identificador propi, i per això va en singular.
/v1/clients/cli_842/preferencies preferències del client (idioma, moneda, butlletí) /v1/comandes/com_5001/pagament el pagament d'aquesta comanda /v1/comandes/com_5001/enviament l'enviament gestionat per RàpidEnviaments /v1/comandes/com_5001/factura la factura d'aquesta comanda /v1/cafes/caf_001/imatge la imatge principal del cafè
Un singleton típicament admet GET, PUT i de vegades DELETE, però no POST (no hi ha col·lecció on afegir), amb l'excepció dels singleton que modelen una acció, que sí que fan servir POST (secció 9).
GET /v1/clients/cli_842/preferencies HTTP/1.1
Host: api.botigaaroma.example
Accept: application/jsonHTTP/1.1 200 OK
Content-Type: application/json
{
"idioma": "ca",
"moneda": "EUR",
"butlleti": true,
"torrefaccioPreferida": "mitja",
"_links": {
"self": { "href": "/v1/clients/cli_842/preferencies" },
"client": { "href": "/v1/clients/cli_842" }
}
}Compte amb el singleton mal fet servir: /clients/cli_842/adreca està bé si el client només en pot tenir una; així que en pugui tenir diverses, es converteix en /clients/cli_842/adreces i això és un canvi trencador (02-07). Si tens dubtes, comença per col·lecció.
- Path parameters davant de query parameters
La confusió més habitual del disseny d'URIs. La regla és senzilla i gairebé mai no falla:
La ruta identifica el recurs. La query string modifica com es retorna la col·lecció.
| Va a la ruta | Va a la query string |
|---|---|
Identificadors de recurs: /cafes/caf_001 |
Filtres: ?origen=Colòmbia&torrefaccio=mitja |
Jerarquia de pertinença: /cafes/caf_001/ressenyes |
Ordenació: ?ordenar=-preuEuros |
Subrecursos singleton: /comandes/com_5001/pagament |
Paginació: ?limit=20&desplacament=40 |
Accions modelades com a subrecurs: /ressenyes/res_101/aprovacio |
Cerca: ?q=yirgacheffe |
Selecció de camps: ?camps=id,nom,preuEuros |
|
Expansió: ?expandir=linies.cafe |
Exemple comparat:
# ✅ Correcte: l'id identifica, va a la ruta
curl https://api.botigaaroma.example/v1/cafes/caf_001
# ❌ Incorrecte: l'id no és un filtre
curl "https://api.botigaaroma.example/v1/cafes?id=caf_001"
# ✅ Correcte: origen és un criteri de selecció sobre la col·lecció
curl "https://api.botigaaroma.example/v1/cafes?origen=Eti%C3%B2pia"
# ❌ Incorrecte: converteix un valor d'atribut en jerarquia
curl https://api.botigaaroma.example/v1/cafes/origen/etiopiaDos matisos que convé conèixer:
- Un filtre que retorna un únic element continua sent una col·lecció.
GET /cafes?nom=Etiòpia Yirgachefferetorna{"dades": [...], "total": 1}, no l'objecte solt, i retorna200amb llista buida si no hi ha coincidències (no404). La raó: el recurs "col·lecció filtrada" existeix encara que estigui buit. - Els paràmetres de query afecten la memòria cau. Cada combinació diferent és una URL diferent i, per tant, una entrada de memòria cau diferent. És un argument més per no multiplicar paràmetres sense necessitat (04-06).
- Disseny d'identificadors
L'identificador que posis a la URI és contracte per sempre. Les opcions:
| Tipus | Exemple | Avantatges | Inconvenients |
|---|---|---|---|
| Enter autoincremental | /cafes/1 |
Curt, llegible, índex barat | Filtra volum de negoci, enumerable, xoca en fusionar bases de dades |
| UUID v4 | /cafes/6f1c... |
No endevinable, generable pel client, únic entre sistemes | Llarg, il·legible, pitjor localitat en índexs |
| UUID v7 / ULID | /cafes/01HQ... |
Ordenable per temps, bon comportament en índexs | Revela l'instant de creació |
| Slug | /cafes/etiopia-yirgacheffe |
Llegible, bo per al SEO | Canvia si canvia el nom; cal gestionar duplicats |
| Id amb prefix | /cafes/caf_001 |
Autodescriptiu, impossible confondre tipus, cercable als logs | Convenció pròpia, no estàndard |
La decisió de la Botiga Aroma: id amb prefix
caf_001, cli_842, com_5001, res_101, cis_77, fac_88, evt_9f2c. És l'estil que va popularitzar Stripe i les raons són molt pràctiques:
- Autodescriptius. En llegir un log o un tiquet,
com_5001s'entén sense context. Amb5001a seques, no saps de què és. - Impossible creuar tipus. Si algú envia
POST /v1/comandesamb{"clientId": "caf_001"}, el servidor detecta el prefix equivocat i respon400ambdades_invalides, en lloc de crear una comanda incoherent. - Opacs per contracte. La documentació diu explícitament: l'identificador és una cadena opaca, no la parsegis, no en suposis la longitud, no suposis que la part numèrica és correlativa. Així podem migrar demà a
caf_01HQ8ZK...sense trencar res a ningú.
En producció, la part que segueix el prefix hauria de ser aleatòria i no correlativa. Els caf_001 i com_5001 del curs són didàctics; en una botiga real, publicar identificadors correlatius té dos problemes:
- Fuita d'informació de negoci. Un competidor que fa una comanda dilluns i una altra divendres sap quantes comandes has rebut aquella setmana. És el clàssic German tank problem.
- Enumeració. Amb ids correlatius, recórrer
com_5001,com_5002,com_5003… és trivial. Que un tercer pugui llegir comandes alienes és una fallada d'autorització, no d'ids —es diu IDOR i es tracta a 04-02—, però els ids endevinables converteixen una fallada puntual en una fuita massiva. La regla és: autoritza sempre, i a més no ho posis fàcil.
Sobre els slugs: són excel·lents per al web públic (botigaaroma.example/cafes/etiopia-yirgacheffe) i dolents com a identitat d'API, perquè canvien. Si els vols, el patró habitual és que el slug sigui un camp més de la representació i un filtre (?slug=etiopia-yirgacheffe), mentre la URI canònica continua sent l'id opac.
- Accions que no són CRUD
Aquí hi ha el problema de disseny més interessant de la lliçó. Moltes operacions del negoci no són "crear, llegir, actualitzar, esborrar":
- Pagar una comanda.
- Anul·lar una comanda.
- Aprovar o rebutjar una ressenya.
- Buidar una cistella.
- Reenviar el correu de confirmació.
Hi ha tres estratègies, i convé entendre-les totes tres abans de triar.
Estratègia A: canviar l'estat amb PATCH
PATCH /v1/ressenyes/res_101 HTTP/1.1
Content-Type: application/merge-patch+json
{ "estat": "publicada" }| A favor | En contra |
|---|---|
| No inventa recursos nous | Els efectes secundaris queden ocults: aprovar dispara correus, recalcula la puntuació mitjana del cafè… |
| CRUD pur, fàcil d'implementar | No es poden passar paràmetres propis de l'acció (motiu del rebuig) |
| Semàntica clara per al client | No distingeix "canviar una dada" d'"executar una transició" |
| Difícil d'autoritzar per separat: qui pot editar pot aprovar |
És acceptable quan la transició és purament un canvi de dada, sense lògica ni efectes.
Estratègia B: verb a la URI (RPC sobre HTTP)
| A favor | En contra |
|---|---|
| Intenció explícita i immediata | Reintrodueix verbs a la URI, just el que evitem a la secció 2 |
| Fàcil d'explicar | Es degrada ràpid: aprovarAmbComentari, aprovarINotificar |
| És el que fan moltes APIs reals | Baixa al nivell 1 de Richardson en aquests endpoints |
Estratègia C: l'acció es converteix en un subrecurs (l'elecció de la Botiga Aroma)
Es busca el substantiu que hi ha darrere del verb: aprovar → una aprovació; pagar → un pagament; anul·lar → una anul·lació. Aquest substantiu és un recurs que es crea amb POST.
POST /v1/ressenyes/res_101/aprovacio HTTP/1.1
Host: api.botigaaroma.example
Authorization: Bearer <token del moderador>
Content-Type: application/json
{ "nota": "Ressenya verificada, compra confirmada" }HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/ressenyes/res_101/aprovacio
{
"estat": "publicada",
"moderadorId": "cli_003",
"dataAprovacio": "2026-03-14T10:32:00Z",
"_links": {
"self": { "href": "/v1/ressenyes/res_101/aprovacio" },
"ressenya": { "href": "/v1/ressenyes/res_101" }
}
}Avantatges, que són just els que buscàvem:
- Sense verbs a la ruta:
aprovacioés un substantiu i el verb el posaPOST. - L'acció admet cos propi: el motiu del rebuig, la referència del pagament, l'import.
- L'acció és un recurs consultable:
GET /v1/comandes/com_5001/pagamentretorna el pagament fet, amb la seva data i la seva referència. - S'autoritza per separat: el permís per crear
/aprovacioés diferent del permís per editar la ressenya (04-03). - Deixa lloc a la idempotència: en ser un
POSTben delimitat, se li pot exigirIdempotency-Key(02-03).
I el mapa d'accions de la Botiga Aroma queda així:
| Acció de negoci | URI | Mètode | Verb evitat |
|---|---|---|---|
| Pagar una comanda | /comandes/{id}/pagament |
POST | pagar |
| Anul·lar una comanda | /comandes/{id}/anullacio |
POST | anul·lar |
| Retornar una comanda | /comandes/{id}/devolucio |
POST | retornar |
| Aprovar una ressenya | /ressenyes/{id}/aprovacio |
POST | aprovar |
| Rebutjar una ressenya | /ressenyes/{id}/rebuig |
POST | rebutjar |
| Respondre a una ressenya | /ressenyes/{id}/respostes |
POST | respondre |
| Buidar una cistella | /cistelles/{id}/linies |
DELETE | buidar |
Fixa't en la darrera fila: abans d'inventar un subrecurs, comprova si un mètode estàndard ja ho expressa. Buidar la cistella és exactament "esborrar totes les seves línies", així que DELETE /cistelles/cis_77/linies és més natural que un POST /cistelles/cis_77/buidat. És l'excepció a la regla de l'apartat 3 sobre no fer servir DELETE en col·leccions, i és legítima perquè aquí la col·lecció està acotada a una cistella concreta i el seu esborrat complet és una operació de negoci real.
- Mapa d'URIs de la Botiga Aroma
Aquest és el resultat de la lliçó i el mapa que la resta del mòdul donarà per bo. Els mètodes hi apareixen per donar context; la seva semàntica exacta és 02-03 i els codis de resposta, 02-04.
| URI | Mètodes | Descripció |
|---|---|---|
/v1/cafes |
GET, POST, HEAD, OPTIONS | Catàleg de cafès; filtrable, ordenable i paginat |
/v1/cafes/{cafeId} |
GET, PUT, PATCH, DELETE, HEAD | Un cafè concret |
/v1/cafes/{cafeId}/imatge |
GET, PUT, DELETE | Imatge principal (singleton, no JSON) |
/v1/cafes/{cafeId}/ressenyes |
GET, POST | Ressenyes d'un cafè (vista imbricada) |
/v1/clients |
GET, POST | Clients registrats (només panell intern) |
/v1/clients/{clientId} |
GET, PATCH, DELETE | Un client concret |
/v1/clients/{clientId}/preferencies |
GET, PUT | Preferències del client (singleton) |
/v1/clients/{clientId}/comandes |
GET | "Les meves comandes" |
/v1/cistelles |
POST | Crea una cistella |
/v1/cistelles/{cistellaId} |
GET, DELETE | Una cistella concreta |
/v1/cistelles/{cistellaId}/linies |
GET, POST, DELETE | Línies de la cistella; DELETE la buida |
/v1/cistelles/{cistellaId}/linies/{cafeId} |
GET, PUT, DELETE | Una línia; PUT fixa la quantitat |
/v1/comandes |
GET, POST | Comandes; POST confirma una cistella |
/v1/comandes/{comandaId} |
GET, PATCH | Una comanda concreta |
/v1/comandes/{comandaId}/linies |
GET | Línies de la comanda (immutables) |
/v1/comandes/{comandaId}/pagament |
GET, POST | Pagament de la comanda (acció + consulta) |
/v1/comandes/{comandaId}/anullacio |
POST | Anul·la la comanda |
/v1/comandes/{comandaId}/devolucio |
POST | Sol·licita devolució |
/v1/comandes/{comandaId}/enviament |
GET, PUT | Enviament; PUT l'actualitza RàpidEnviaments |
/v1/comandes/{comandaId}/factura |
GET | Factura (JSON o PDF, segons Accept) |
/v1/ressenyes |
GET | Totes les ressenyes (vista plana, filtrable) |
/v1/ressenyes/{ressenyaId} |
GET, PATCH, DELETE | Una ressenya (URI canònica) |
/v1/ressenyes/{ressenyaId}/aprovacio |
POST | Aprova la ressenya |
/v1/ressenyes/{ressenyaId}/rebuig |
POST | Rebutja la ressenya |
/v1/ressenyes/{ressenyaId}/respostes |
GET, POST | Respostes de la botiga a la ressenya |
Decisions anotades per no oblidar-les:
- No hi ha
POST /v1/ressenyes: una ressenya sempre neix associada a un cafè, així que només es crea a/cafes/{cafeId}/ressenyes. La vista plana és de només lectura i serveix al panell intern. - No hi ha
DELETE /v1/comandes/{id}: una comanda no s'esborra, s'anul·la. L'històric comptable és sagrat. /v1/origenss'ha considerat i descartat per a la v1: l'origen és un atribut i es filtra amb?origen=. Si algun dia té dades pròpies (altitud, cooperativa, foto), serà una col·lecció i es podrà afegir sense trencar res (02-07).
Errors Comuns i Consells
- Ficar el verb a la ruta "només per a aquesta operació". Comences amb una excepció i acabes amb vint. Si necessites una acció, busca'n el substantiu.
- Imbricar per costum.
/clients/cli_842/comandes/com_5001obliga el servidor a validar que aquesta comanda és d'aquest client i el client a conèixer dos ids per demanar-ne un. Imbrica per llistar, fes servir la URI canònica per a l'element. - Fer servir la query string per identificar.
/cafes?id=caf_001trenca la memòria cau per URL, complica els enllaçosselfi no permet subrecursos. - Posar
.jsonal final. Barreja identitat i format; per a això hi haAccept. - Pluralitzar malament.
/cafesi/cafe/caf_001convivint és el bug de documentació més freqüent del món. - Exposar l'id de base de dades per comoditat. Canviar de motor o fusionar entorns t'obligarà a trencar el contracte. Un id opac et deixa llibertat.
- Consell: escriu les URIs abans que el codi i llegeix-les en veu alta. Si en llegir
/comandes/com_5001/anullacioambPOSTs'entén sense explicar-ho, està ben dissenyada. - Consell: mantén una taula com la de la secció 10 al repositori. És l'índex del contracte i el lloc on es discuteix qualsevol endpoint nou abans d'existir.
Exercicis
Exercici 1: corregir un conjunt d'URIs
Un equip ha proposat aquestes rutes per a la Botiga Aroma. Corregeix-les i justifica cada canvi.
GET /v1/api/getCafes.json POST /v1/Cafe/crear GET /v1/cafes?id=caf_001 POST /v1/cafes/caf_001/ressenyes/res_101/aprovar GET /v1/clients/cli_842/comandes/com_5001/linies/1/cafe/caf_001/ressenyes DELETE /v1/cistelles/cis_77/buidarCistella GET /v1/cafes/origen/etiopia/torrefaccio/clar
Exercici 2: modelar una acció nova
La Botiga Aroma vol que un client pugui regalar una comanda: en confirmar-la indica el correu del destinatari i un missatge, i el sistema envia un avís i amaga el preu a l'albarà.
Modela aquesta funcionalitat amb les tres estratègies de la secció 9 (PATCH, verb a la URI, subrecurs), mostra la petició HTTP de cadascuna i tria la que encaixa amb la guia d'estil, justificant-ho.
Exercici 3: decidir imbricacions
Per a cada necessitat, decideix la URI i digues si has imbricat o no i per què:
- El panell intern vol veure totes les línies venudes del cafè
caf_001en el darrer mes. - Aroma Mòbil vol les ressenyes escrites pel client
cli_842. - RàpidEnviaments vol actualitzar l'estat de l'enviament de la comanda
com_5001. - La SPA vol l'històric de canvis de preu de
caf_001.
Solucions
Solució 1
| Proposta | Correcció | Motiu |
|---|---|---|
GET /v1/api/getCafes.json |
GET /v1/cafes |
Sobra /api (ja és a l'amfitrió), sobra el verb get (el posa el mètode) i sobra .json (ho negocia Accept) |
POST /v1/Cafe/crear |
POST /v1/cafes |
Minúscules, plural i sense verb: POST sobre la col·lecció ja significa crear |
GET /v1/cafes?id=caf_001 |
GET /v1/cafes/caf_001 |
L'identificador va a la ruta; la query filtra col·leccions |
POST /v1/cafes/caf_001/ressenyes/res_101/aprovar |
POST /v1/ressenyes/res_101/aprovacio |
Verb → substantiu, i es retalla a la URI canònica de la ressenya: el cafè és redundant |
GET /v1/clients/.../cafe/caf_001/ressenyes |
GET /v1/cafes/caf_001/ressenyes |
Cinc nivells d'imbricació innecessaris: la ressenya depèn del cafè, no de la comanda del client |
DELETE /v1/cistelles/cis_77/buidarCistella |
DELETE /v1/cistelles/cis_77/linies |
El mètode estàndard ja expressa l'acció sobre la subcol·lecció |
GET /v1/cafes/origen/etiopia/torrefaccio/clar |
GET /v1/cafes?origen=Etiòpia&torrefaccio=clar |
Origen i torrefacció són atributs, no jerarquia: són filtres |
Solució 2
Estratègia A — PATCH sobre la comanda:
PATCH /v1/comandes/com_5001
Content-Type: application/merge-patch+json
{ "regal": { "destinatari": "[email protected]", "missatge": "Felicitats!" } }Funciona, però l'enviament de l'avís queda com a efecte ocult d'un canvi de camp, i no hi ha on consultar després si l'avís es va enviar.
Estratègia B — verb a la URI:
POST /v1/comandes/com_5001/regalar
Content-Type: application/json
{ "destinatari": "[email protected]", "missatge": "Felicitats!" }Intenció claríssima, però verb a la ruta: incompleix la guia d'estil i obre la porta a regalarSenseAvis.
Estratègia C — subrecurs (escollida):
POST /v1/comandes/com_5001/regal
Content-Type: application/json
{ "destinatari": "[email protected]", "missatge": "Felicitats!", "ocultarPreu": true }HTTP/1.1 201 Created
Location: /v1/comandes/com_5001/regal
{
"destinatari": "[email protected]",
"missatge": "Felicitats!",
"ocultarPreu": true,
"avisEnviat": true,
"dataAvis": "2026-03-14T10:35:00Z",
"_links": { "self": { "href": "/v1/comandes/com_5001/regal" },
"comanda": { "href": "/v1/comandes/com_5001" } }
}És l'escollida: substantiu (regal), cos propi per als paràmetres de l'acció, consultable després amb GET, i es pot cancel·lar amb DELETE /v1/comandes/com_5001/regal mentre la comanda no s'hagi enviat. Encaixa exactament amb /pagament, /anullacio i /aprovacio.
Solució 3
GET /v1/linies-comanda?cafeId=caf_001&dataDes=2026-02-14— sense imbricar. El que es busca creua totes les comandes, així que la jerarquia/comandes/{id}/liniesno serveix: cal una col·lecció plana de primer nivell consultable amb filtres. Compte amb la temptació d'escriure/v1/comandes/linies: col·lidiria amb/v1/comandes/{comandaId}, perquè l'encaminador no pot distingir un id anomenatliniesd'un segment fix. (Alternativa raonable: un recurs d'informes/v1/vendes?cafeId=..., si el panell necessita agregats en lloc de línies soltes.)GET /v1/ressenyes?clientId=cli_842— sense imbricar sota el cafè, perquè el criteri és l'autor. Imbricar-ho com a/clients/cli_842/ressenyestambé seria defensable si "les meves ressenyes" fos una pantalla pròpia d'Aroma Mòbil; totes dues compleixen la regla, i en aquest cas convindria triar-ne una de sola per no duplicar contracte.PUT /v1/comandes/com_5001/enviament— imbricat i singleton: un enviament no existeix fora de la seva comanda i només n'hi ha un.PUTperquè RàpidEnviaments envia l'estat complet de l'enviament a cada actualització (02-03).GET /v1/cafes/caf_001/historic-preus— imbricat: l'històric no té sentit fora del seu cafè i és de només lectura. Fixa't en elkebab-casea la paraula composta. Si l'històric es consultés globalment per a tot el catàleg, es convertiria en/v1/historic-preus?cafeId=caf_001.
Conclusió
Les URIs són la cara pública i més duradora de l'API, i ara tens regles concretes per dissenyar-les: substantius en plural i minúscules, kebab-case per a paraules compostes, sense extensions ni verbs, amb imbricació només quan expressa pertinença real i sense passar de dos nivells, singleton en singular per a allò que és únic, identificació a la ruta i modificació de col·leccions a la query string, i identificadors opacs amb prefix que no filtrin informació de negoci. I sobretot tens resolt el problema que desconcerta tothom: les accions que no són CRUD es modelen com a subrecursos creats amb POST, cosa que dona a cada acció cos propi, consulta posterior i autorització independent. El mapa d'URIs de la Botiga Aroma està tancat.
Ja sabem quins recursos existeixen i on viuen. Falta dir amb precisió què es pot fer amb cadascun. A la lliçó següent, 02-03 Mètodes HTTP, recorrerem GET, POST, PUT, PATCH, DELETE, HEAD i OPTIONS aplicats a aquest mapa; entendrem per què la seguretat i la idempotència són molt més que teoria quan RàpidEnviaments reintenta una petició o un client prem dues vegades el botó de pagar; compararem a fons PUT davant de PATCH amb JSON Merge Patch i JSON Patch; i dissenyarem les claus d'idempotència del pagament d'una comanda.
Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful
Mòdul 1: Introducció a les APIs RESTful
- Què és una API?
- Història i evolució de les APIs
- Fonaments d'HTTP per a APIs
- Principis bàsics de REST
- Model de maduresa de Richardson i HATEOAS
- REST vs. SOAP
- REST davant de GraphQL, gRPC i webhooks
Mòdul 2: Disseny d'APIs RESTful
- Principis de disseny d'APIs RESTful
- Recursos i URIs
- Mètodes HTTP
- Codis d'estat HTTP
- Representacions, capçaleres i negociació de contingut
- Filtratge, ordenació, paginació i cerca
- Versionat d'APIs
- Documentació d'APIs
Mòdul 3: Desenvolupament d'APIs RESTful
- Configuració de l'entorn de desenvolupament
- Creació d'un servidor bàsic
- Gestió de peticions i respostes
- Validació de dades d'entrada
- Persistència i capa d'accés a dades
- Autenticació i autorització
- Gestió d'errors
- Proves i validació
Mòdul 4: Bones Pràctiques i Seguretat
- Bones pràctiques en el disseny d'APIs
- Seguretat en APIs RESTful
- OAuth 2.0 i OpenID Connect a la pràctica
- Rate limiting i throttling
- CORS i polítiques de seguretat
- Memòria cau HTTP i rendiment
- Observabilitat: logs, mètriques i traces
Mòdul 5: Eines i Frameworks
- Postman per a proves d'APIs
- Swagger i OpenAPI per a documentació
- Frameworks populars per a APIs RESTful
- Contractes, mocks i proves automatitzades d'API
- Integració contínua i desplegament
- API gateways i portals de desenvolupador
