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

  1. Què és un recurs i què és una URI
  2. Substantius i no verbs
  3. Col·leccions i elements
  4. Regles d'anomenament
  5. Jerarquia i imbricació
  6. Recursos singleton
  7. Path parameters davant de query parameters
  8. Disseny d'identificadors
  9. Accions que no són CRUD
  10. Mapa d'URIs de la Botiga Aroma

  1. 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/factura en 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_001 deixa de funcionar perquè s'ha refactoritzat la base de dades, hem trencat el contracte.

  1. 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 /obtenirCafes repeteix el verb i, pitjor encara, permet la incoherència POST /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.

  1. 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è

  1. 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.

✅ /cafes            /cafes/caf_001
❌ /cafes            /cafe/caf_001
❌ /llistaCafes      /cafes/caf_001

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)

✅ /notes-tast          /metodes-pagament    /comandes/com_5001/nota-regal
❌ /notesTast           /notes_tast          /NotesTast

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

✅ /cafes/caf_001                  amb capçalera Accept: application/json
❌ /cafes/caf_001.json
❌ /cafes/caf_001.xml

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

❌ /api/v1/cafesController/getAll
❌ /v1/tbl_cafes
❌ /v1/cafesDTO

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.

  1. 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.

✅ /comandes/com_5001/linies
❌ /clients/cli_842/comandes/com_5001/linies/lin_3/cafe/caf_001

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_moderacio

Regles 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ç self de 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/ressenyes no necessita repetir cafeId al 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)"]

  1. 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/json
HTTP/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ó.

  1. 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/etiopia

Dos matisos que convé conèixer:

  • Un filtre que retorna un únic element continua sent una col·lecció. GET /cafes?nom=Etiòpia Yirgacheffe retorna {"dades": [...], "total": 1}, no l'objecte solt, i retorna 200 amb llista buida si no hi ha coincidències (no 404). 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).

  1. 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:

  1. Autodescriptius. En llegir un log o un tiquet, com_5001 s'entén sense context. Amb 5001 a seques, no saps de què és.
  2. Impossible creuar tipus. Si algú envia POST /v1/comandes amb {"clientId": "caf_001"}, el servidor detecta el prefix equivocat i respon 400 amb dades_invalides, en lloc de crear una comanda incoherent.
  3. 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.

  1. 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)

POST /v1/ressenyes/res_101/aprovar
POST /v1/comandes/com_5001/pagar
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 posa POST.
  • 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/pagament retorna 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 POST ben delimitat, se li pot exigir Idempotency-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.

  1. 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/origens s'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_5001 obliga 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_001 trenca la memòria cau per URL, complica els enllaços self i no permet subrecursos.
  • Posar .json al final. Barreja identitat i format; per a això hi ha Accept.
  • Pluralitzar malament. /cafes i /cafe/caf_001 convivint é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/anullacio amb POST s'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è:

  1. El panell intern vol veure totes les línies venudes del cafè caf_001 en el darrer mes.
  2. Aroma Mòbil vol les ressenyes escrites pel client cli_842.
  3. RàpidEnviaments vol actualitzar l'estat de l'enviament de la comanda com_5001.
  4. 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

  1. 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}/linies no 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 anomenat linies d'un segment fix. (Alternativa raonable: un recurs d'informes /v1/vendes?cafeId=..., si el panell necessita agregats en lloc de línies soltes.)
  2. GET /v1/ressenyes?clientId=cli_842 — sense imbricar sota el cafè, perquè el criteri és l'autor. Imbricar-ho com a /clients/cli_842/ressenyes també 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.
  3. 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. PUT perquè RàpidEnviaments envia l'estat complet de l'enviament a cada actualització (02-03).
  4. 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 el kebab-case a 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

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats