Ja tenim el mapa de recursos i les seves URIs. Ara cal dir què es pot fer amb cadascun, i en REST això ho diu el mètode HTTP. Aquesta lliçó recorre en profunditat GET, POST, PUT, PATCH, DELETE, HEAD i OPTIONS aplicats a la Botiga Aroma, amb petició i resposta completes de cadascun. Després entra en els dos conceptes que separen una API que aguanta la realitat d'una que es trenca en producció: la seguretat (safety) i la idempotència. No són subtileses acadèmiques: són la diferència entre que RàpidEnviaments reintenti una petició sense conseqüències o que un client acabi pagant dues vegades la mateixa comanda. Tancarem amb PUT davant de PATCH a fons, l'esborrat lògic, les claus d'idempotència i les operacions en lot.

Contingut

  1. Els mètodes d'una ullada
  2. GET: llegir sense efectes
  3. POST: crear i executar accions
  4. PUT: substitució total
  5. PATCH: modificació parcial
  6. PUT davant de PATCH, i què tria la Botiga Aroma
  7. DELETE: esborrat físic i lògic
  8. HEAD i OPTIONS
  9. Seguretat i idempotència
  10. Claus d'idempotència per al pagament
  11. Operacions en lot

  1. Els mètodes d'una ullada

Mètode Què significa Cos a la petició? Cos a la resposta? Segur Idempotent
GET Obtenir la representació No
HEAD Com GET, només capçaleres No No
OPTIONS Què es pot fer aquí No Opcional
POST Crear subordinat o executar acció No No
PUT Substituir del tot Sí (o 204) No
PATCH Modificar parcialment Sí (o 204) No Depèn
DELETE Eliminar No (normalment) Opcional (o 204) No

Existeixen dos mètodes més i aquí només els esmentem: TRACE (eco de la petició, es deshabilita per seguretat) i CONNECT (túnels de proxy). Cap API REST no els fa servir.

Si un client fa servir un mètode que el recurs no admet, la resposta correcta és 405 Method Not Allowed amb la capçalera Allow; si el servidor no coneix el mètode en absolut, és 501 Not Implemented. El detall dels codis és la lliçó següent.

  1. GET: llegir sense efectes

GET obté la representació d'un recurs. És el mètode més utilitzat de qualsevol API i l'únic que es pot posar a la memòria cau amb garanties (04-06).

curl -i "https://api.botigaaroma.example/v1/cafes/caf_001" \
  -H "Accept: application/json"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: ca
Cache-Control: public, max-age=300

{
  "id": "caf_001",
  "nom": "Etiòpia Yirgacheffe",
  "origen": "Etiòpia",
  "torrefaccio": "clar",
  "preuEuros": 14.50,
  "estoc": 120,
  "notesTast": ["cítric", "floral", "te negre"],
  "dataCreacio": "2026-01-15T08:30:00Z",
  "_links": {
    "self": { "href": "/v1/cafes/caf_001" },
    "ressenyes": { "href": "/v1/cafes/caf_001/ressenyes" }
  }
}

Regles de disseny per a GET a la Botiga Aroma:

  • Mai no modifica res. Ni tan sols un comptador "discret" de visites: els prefetchers dels navegadors, els rastrejadors i els proxys fan GET pel seu compte. Si necessites registrar la visita, fes-ho fora de la semàntica del recurs o amb un POST explícit.
  • No porta cos. Tècnicament HTTP no ho prohibeix, però molts intermediaris el descarten i alguns clients ni tan sols l'envien. Consultes complexes per cos → POST (02-06).
  • Un GET sobre una col·lecció buida és 200 amb {"dades": [], "total": 0}, no 404. La col·lecció existeix encara que no tingui elements.
  • Un GET sobre un element inexistent és 404 amb l'error de negoci corresponent:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "codi": "cafe_no_trobat",
    "missatge": "No existeix cap cafè amb l'identificador 'caf_999'.",
    "detalls": []
  }
}

  1. POST: crear i executar accions

POST és el mètode de propòsit general: crea un recurs subordinat a la col·lecció o executa l'acció que representa un subrecurs (02-02).

3.1. Crear un element en una col·lecció

curl -i -X POST "https://api.botigaaroma.example/v1/cafes" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "nom": "Colòmbia Huila",
    "origen": "Colòmbia",
    "torrefaccio": "mitja",
    "preuEuros": 12.90,
    "estoc": 80,
    "notesTast": ["xocolata", "caramel", "taronja"]
  }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/cafes/caf_002

{
  "id": "caf_002",
  "nom": "Colòmbia Huila",
  "origen": "Colòmbia",
  "torrefaccio": "mitja",
  "preuEuros": 12.90,
  "estoc": 80,
  "notesTast": ["xocolata", "caramel", "taronja"],
  "dataCreacio": "2026-03-14T09:12:00Z",
  "_links": { "self": { "href": "/v1/cafes/caf_002" } }
}

Tres punts del contracte:

  1. L'identificador l'assigna el servidor. El client no envia id; si l'envia, es rebutja amb 400.
  2. Location és obligatòria en tota creació. Conté la URI del recurs creat. És el que permet a un client encadenar operacions sense endevinar URLs.
  3. Es retorna el recurs complet al cos, no només l'id: estalvia un GET immediat i mostra els camps calculats pel servidor (dataCreacio).

3.2. Executar una acció

curl -i -X POST "https://api.botigaaroma.example/v1/comandes/com_5001/pagament" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50" \
  -H "Content-Type: application/json" \
  -d '{ "metode": "targeta", "tokenTargeta": "tok_visa_4242" }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/comandes/com_5001/pagament

{
  "estat": "pagat",
  "importEuros": 29.00,
  "metode": "targeta",
  "referencia": "pay_7712",
  "dataPagament": "2026-03-14T10:32:00Z",
  "_links": {
    "self": { "href": "/v1/comandes/com_5001/pagament" },
    "comanda": { "href": "/v1/comandes/com_5001" },
    "factura": { "href": "/v1/comandes/com_5001/factura" }
  }
}

3.3. POST no és idempotent

És la característica que defineix POST i la font de la majoria dels incidents reals:

# Executat dues vegades, crea DOS cafès diferents
curl -X POST .../v1/cafes -d '{"nom": "Colòmbia Huila", ...}'   # → caf_002
curl -X POST .../v1/cafes -d '{"nom": "Colòmbia Huila", ...}'   # → caf_003

Quan això sigui inacceptable (pagaments, comandes), hi ha dos remeis: la capçalera Idempotency-Key (secció 10) o retornar 409 Conflict si detectes un duplicat de negoci. La Botiga Aroma fa servir totes dues coses.

  1. PUT: substitució total

PUT diu: "en aquesta URI hi ha de quedar exactament aquesta representació". És una substitució completa, no una fusió.

curl -i -X PUT "https://api.botigaaroma.example/v1/cafes/caf_001" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "nom": "Etiòpia Yirgacheffe",
    "origen": "Etiòpia",
    "torrefaccio": "clar",
    "preuEuros": 15.20,
    "estoc": 120,
    "notesTast": ["cítric", "floral", "te negre"]
  }'
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "caf_001",
  "nom": "Etiòpia Yirgacheffe",
  "origen": "Etiòpia",
  "torrefaccio": "clar",
  "preuEuros": 15.20,
  "estoc": 120,
  "notesTast": ["cítric", "floral", "te negre"],
  "dataCreacio": "2026-01-15T08:30:00Z"
}

El perill de PUT: si el client vol apujar el preu i envia només {"preuEuros": 15.20}, un PUT correcte deixa el cafè sense nom, sense origen, sense torrefacció, sense estoc i sense notes de tast. És l'error de principiant més car d'aquesta lliçó. La Botiga Aroma ho mitiga rebutjant amb 400 els PUT als quals els falten camps obligatoris, però la semàntica continua sent "substitueix-ho tot".

PUT sí que és idempotent: enviar-lo deu vegades deixa el recurs exactament igual que enviar-lo una vegada. Per això és el mètode que fa servir RàpidEnviaments per actualitzar l'enviament, els reintents del qual són freqüents:

PUT /v1/comandes/com_5001/enviament HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token de RàpidEnviaments>

{
  "estat": "en_repartiment",
  "codiSeguiment": "RE-9981234",
  "dataEstimada": "2026-03-16T12:00:00Z"
}

Un detall poc conegut: PUT també pot crear un recurs si el client coneix la URI per endavant. La Botiga Aroma ho fa exactament en un lloc, les línies de cistella:

PUT /v1/cistelles/cis_77/linies/caf_002 HTTP/1.1
Content-Type: application/json

{ "quantitat": 3 }

Si la línia no existia es crea (201 Created); si existia se substitueix la quantitat (200 OK). I és idempotent: prémer tres vegades "posar 3 unitats" deixa 3 unitats, no 9. Compara-ho amb POST /linies amb {"cafeId": "caf_002", "quantitat": 1}, que suma una unitat cada vegada que s'executa: totes dues operacions tenen sentit, però signifiquen coses diferents i convé tenir-ho escrit a la documentació.

  1. PATCH: modificació parcial

PATCH envia només allò que canvia. La pregunta interessant és en quin format, perquè PATCH no en defineix cap: el defineix el Content-Type.

5.1. JSON Merge Patch (RFC 7386)

El document enviat es fusiona amb el recurs. Un valor null significa "esborra aquest camp".

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "preuEuros": 15.20,
  "estoc": 95
}

Resultat: es canvien preuEuros i estoc; tota la resta es queda com estava.

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "notesTast": null }

Resultat: s'elimina el camp notesTast.

La limitació de Merge Patch són els arrays: se substitueixen sencers, mai no s'editen per posició. Per afegir una nota de tast cal enviar la llista completa:

{ "notesTast": ["cítric", "floral", "te negre", "bergamota"] }

I el corol·lari incòmode: amb Merge Patch no es pot posar un camp a null de debò, perquè null està reservat per a "esborrar".

5.2. JSON Patch (RFC 6902)

És una llista d'operacions sobre el document, expressades amb JSON Pointer.

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/preuEuros", "value": 15.20 },
  { "op": "add",     "path": "/notesTast/-", "value": "bergamota" },
  { "op": "remove",  "path": "/notesTast/0" },
  { "op": "test",    "path": "/estoc", "value": 120 }
]

Les operacions són add, remove, replace, move, copy i test. path fa servir JSON Pointer (/notesTast/- significa "al final de l'array"). Si qualsevol operació falla, no se n'aplica cap: és atòmic.

L'operació test és la joia amagada: "aplica això només si estoc continua valent 120". És control de concurrència optimista dins del mateix cos, complementari al que es fa amb If-Match i ETags (04-06).

5.3. Comparació

Criteri JSON Merge Patch (7386) JSON Patch (6902)
Content-Type application/merge-patch+json application/json-patch+json
Llegibilitat Molt alta: sembla el recurs Baixa: cal llegir operacions
Editar un element d'array No (se substitueix l'array) Sí, per índex
Posar un camp a null Impossible (null = esborrar) Sí (replace amb value: null)
Condicions prèvies No Sí, amb test
Reordenar, moure, copiar No
Idempotència Sí, sempre No sempre (add a /array/- acumula)
Facilitat per al client Molt alta Mitjana
Adopció Majoritària Nínxols (documents complexos, Kubernetes)

5.4. La decisió de la Botiga Aroma

JSON Merge Patch com a format oficial, amb Content-Type: application/merge-patch+json. Raons: els recursos són plans i petits, els clients són majoritàriament frontends que ja tenen l'objecte a la memòria, la llegibilitat de les peticions facilita el suport, i el 100 % dels casos d'ús reals són "canviar dos o tres camps". Els arrays de la Botiga Aroma (notesTast, linies) són curts i substituir-los sencers no és cap problema.

A més, s'accepta application/json a seques i es tracta com a Merge Patch, perquè és el que envien moltes eines per defecte; un Content-Type diferent d'aquests dos es rebutja amb 415 Unsupported Media Type. Que l'API no admet JSON Patch queda escrit a la documentació, perquè ningú no ho intenti.

  1. PUT davant de PATCH, i què tria la Botiga Aroma

Criteri PUT PATCH
Semàntica Substitueix el recurs complet Aplica canvis parcials
Camps absents S'esborren o es posen per defecte Es mantenen
Mida del cos Tot el recurs Només allò que canvia
Idempotent Sempre Amb Merge Patch, sí
Risc de trepitjar canvis d'un altre Alt: envies camps que no volies tocar Baix: només toques allò teu
Pot crear el recurs No (404 si no existeix)
Ús típic Formularis complets, sincronització Edicions puntuals

Assignació al mapa de la Botiga Aroma:

Recurs Mètode d'actualització Motiu
/cafes/{id} PUT i PATCH El panell edita fitxes completes; els ajustos de preu i estoc són parcials
/clients/{id} Només PATCH Mai no se substitueix un client sencer: hi ha camps que el client no veu
/clients/{id}/preferencies Només PUT Són quatre camps i el formulari els envia tots
/cistelles/{id}/linies/{cafeId} Només PUT Fixar la quantitat ha de ser idempotent
/comandes/{id} Només PATCH Només hi ha camps molt concrets editables (adreça abans de l'enviament)
/comandes/{id}/enviament Només PUT RàpidEnviaments envia l'estat complet i reintenta
/ressenyes/{id} Només PATCH Es corregeix el comentari; l'estat es canvia amb /aprovacio o /rebuig

  1. DELETE: esborrat físic i lògic

curl -i -X DELETE "https://api.botigaaroma.example/v1/cistelles/cis_77/linies/caf_002" \
  -H "Authorization: Bearer <token>"
HTTP/1.1 204 No Content

7.1. Físic davant de lògic

Esborrat físic Esborrat lògic (soft delete)
Què fa Elimina la fila Marca eliminat = true i l'amaga
Reversible No
Auditoria i històric Es perden Es conserven
Integritat referencial Pot trencar comandes antigues Intacta
Cost Cap Filtrar a totes les consultes, creixement de taules

La Botiga Aroma fa servir esborrat lògic per als cafès (una comanda del 2025 ha de poder continuar mostrant el cafè que es va vendre) i per als clients (obligacions fiscals), i esborrat físic per a les línies de cistella (dades efímeres sense valor històric). Les comandes no s'esborren mai: s'anul·len amb POST /comandes/{id}/anullacio.

Punt clau: l'esborrat lògic és invisible al contracte. Des de fora, després d'un DELETE /v1/cafes/caf_001, el cafè ja no apareix a /v1/cafes i GET /v1/cafes/caf_001 respon 404 (o 410, veure més avall). Que per dins la fila continuï allà és cosa de la capa de persistència (03-05).

7.2. I si esborro dues vegades?

Aquí hi ha un debat clàssic. Segon DELETE sobre un recurs ja esborrat:

  • 404 Not Found: literal. El recurs no existeix ara, i és la resposta més comuna.
  • 204 No Content: pragmàtica. L'objectiu del client ("que no existeixi") ja es compleix.

Cap de les dues no trenca la idempotència, i convé entendre bé per què: idempotència significa que l'efecte sobre el servidor és el mateix després d'una o N peticions, no que el codi de resposta sigui idèntic. Després d'un o cinc DELETE, el recurs està esborrat: això és idempotent.

Decisió de la Botiga Aroma: 404, perquè distingeix "l'he esborrat jo ara" de "això ja no hi era", informació útil per depurar clients amb reintents. I per als cafès retirats del catàleg definitivament es fa servir 410 Gone, que diu "va existir i no tornarà" (02-04).

DELETE no ha de portar cos de petició. Si necessites paràmetres per esborrar (un motiu, una data efectiva), és senyal que estàs davant d'una acció, i les accions es modelen com a subrecurs amb POST (02-02).

  1. HEAD i OPTIONS

8.1. HEAD

Idèntic a GET, però el servidor retorna només les capçaleres. Serveix per comprovar existència, mida o frescor sense descarregar el cos.

curl -I "https://api.botigaaroma.example/v1/comandes/com_5001/factura"
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 48213
Last-Modified: Sat, 14 Mar 2026 10:33:00 GMT

Aroma Mòbil el fa servir per saber si val la pena descarregar una factura de 48 KB amb la connexió actual. La regla d'or: HEAD ha de retornar exactament les mateixes capçaleres que retornaria GET; si el teu HEAD retorna Content-Length: 0, està mal implementat.

8.2. OPTIONS

Pregunta què es pot fer amb un recurs. La resposta obligatòria és la capçalera Allow.

curl -i -X OPTIONS "https://api.botigaaroma.example/v1/cafes/caf_001"
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
Accept-Patch: application/merge-patch+json

Accept-Patch és la manera estàndard d'anunciar quins formats de PATCH accepta el recurs: aquí queda publicada, al mateix protocol, la decisió de la secció 5.4.

L'ús massiu d'OPTIONS a la pràctica no el fan les persones, sinó els navegadors: és la petició de comprovació prèvia (preflight) de CORS, que la SPA de la Botiga Aroma dispara abans de cada PATCH o DELETE amb capçaleres personalitzades. Aquest mecanisme complet s'estudia a 04-05.

  1. Seguretat i idempotència

Les dues propietats més importants d'aquesta lliçó.

  • Segur (safe): el mètode no modifica l'estat del servidor. És una lectura. Qualsevol intermediari el pot repetir, precarregar o posar a la memòria cau sense demanar permís.
  • Idempotent: executar-lo N vegades deixa el servidor en el mateix estat que executar-lo una vegada. Compte: el mateix estat, no la mateixa resposta.
graph TD
    A["El client envia POST /comandes/com_5001/pagament"] --> B["El servidor la rep<br/>i cobra 29,00 €"]
    B --> C["La resposta es perd:<br/>timeout de xarxa"]
    C --> D{"El client reintenta?"}
    D -->|"Sense Idempotency-Key"| E["Segon cobrament de 29,00 €<br/><b>client cobrat dues vegades</b>"]
    D -->|"Amb Idempotency-Key"| F["El servidor reconeix la clau<br/>i retorna la resposta original<br/><b>un sol cobrament</b>"]

Tot mètode segur és idempotent; el contrari no és cert (DELETE és idempotent però no segur).

Per què importa de debò, amb els tres escenaris de la Botiga Aroma:

  1. Reintents de RàpidEnviaments. El seu client HTTP reintenta automàticament davant d'un 503 o d'un timeout. Com que actualitza l'enviament amb PUT (idempotent), tres reintents deixen el mateix enviament. Si ho haguéssim modelat com a POST /comandes/{id}/esdeveniments-enviament, tindríem tres esdeveniments duplicats.
  2. Timeouts de xarxa al mòbil. Aroma Mòbil perd cobertura just després d'enviar la petició. El client no sap si el servidor la va processar. Només pot reintentar sense por si el mètode és idempotent.
  3. Doble clic a "pagar". El cas més humà de tots. POST no és idempotent, així que el remei no pot venir del mètode: ve de la secció següent.

Conseqüència de disseny: com més idempotent sigui la teva API, més barat és operar-la, perquè els reintents automàtics (del client, del balancejador, de la passarel·la) deixen de ser perillosos.

  1. Claus d'idempotència per al pagament

Una clau d'idempotència és un identificador únic que el client genera abans d'enviar la petició i repeteix a cada reintent d'aquesta mateixa operació lògica.

Flux complet de /comandes/com_5001/pagament:

# El client genera un UUID i el desa ABANS d'enviar res
CLAU="5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50"

curl -i -X POST "https://api.botigaaroma.example/v1/comandes/com_5001/pagament" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: $CLAU" \
  -H "Content-Type: application/json" \
  -d '{ "metode": "targeta", "tokenTargeta": "tok_visa_4242" }'

Què fa el servidor:

  1. Busca la clau. Si no existeix, la registra juntament amb una empremta del cos, processa el pagament i desa la resposta durant 24 hores.
  2. Si existeix i el cos coincideix: no torna a cobrar; retorna la resposta desada, amb Idempotent-Replay: true perquè el client sàpiga que és una repetició.
  3. Si existeix i el cos és diferent: respon 422 Unprocessable Content amb codi clau_idempotencia_reutilitzada. És una protecció contra errors del client: la mateixa clau no pot significar dues operacions diferents.
  4. Si la petició original encara s'està processant: respon 409 Conflict amb operacio_en_curs i Retry-After: 2.

Resposta del reintent:

HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/comandes/com_5001/pagament
Idempotent-Replay: true

{
  "estat": "pagat",
  "importEuros": 29.00,
  "referencia": "pay_7712",
  "dataPagament": "2026-03-14T10:32:00Z"
}

Contracte de la Botiga Aroma sobre Idempotency-Key:

Endpoint Clau? Comportament sense clau
POST /comandes Obligatòria 400 amb clau_idempotencia_requerida
POST /comandes/{id}/pagament Obligatòria 400 amb clau_idempotencia_requerida
POST /comandes/{id}/anullacio Recomanada Es processa; si ja està anul·lada, 409 comanda_ja_anullada
POST /cafes Opcional Es processa (podria crear duplicats)
POST /cafes/{id}/ressenyes Opcional Es processa

I una segona línia de defensa que no depèn del client: el mateix recurs protegeix la seva transició. Un segon pagament de la mateixa comanda, encara que vingui amb una altra clau, troba la comanda en estat pagat i respon 409 Conflict amb comanda_ja_pagada. La idempotència protegeix dels reintents tècnics; la màquina d'estats protegeix dels errors lògics. Calen totes dues.

  1. Operacions en lot

Tard o d'hora algú demanarà "actualitzar l'estoc de 200 cafès de cop". Les opcions:

a) N peticions individuals. Semànticament perfecte, fàcil de posar a la memòria cau i de reintentar. Amb HTTP/2 i connexions multiplexades, 200 PATCH petits són molt més viables del que la gent suposa.

b) Un endpoint de lot.

POST /v1/cafes/actualitzacions-lot HTTP/1.1
Content-Type: application/json

{
  "operacions": [
    { "id": "caf_001", "canvis": { "estoc": 95 } },
    { "id": "caf_002", "canvis": { "estoc": 0 } },
    { "id": "caf_999", "canvis": { "estoc": 10 } }
  ]
}

I aquí apareix el problema fonamental del lot: quin codi retornes si una de les tres falla? No és 200, perquè alguna cosa ha fallat. No és 400, perquè dues han funcionat. La resposta habitual és 207 Multi-Status, un codi que ve de WebDAV, amb el detall per element:

HTTP/1.1 207 Multi-Status
Content-Type: application/json

{
  "dades": [
    { "id": "caf_001", "estat": 200 },
    { "id": "caf_002", "estat": 200 },
    { "id": "caf_999", "estat": 404,
      "error": { "codi": "cafe_no_trobat", "missatge": "No existeix 'caf_999'.", "detalls": [] } }
  ],
  "total": 3
}

Riscos que cal tenir presents abans d'acceptar un endpoint de lot:

  • Atomicitat ambigua: és tot o res, o parcial? Cal decidir-ho i documentar-ho; totes dues opcions són defensables i la confusió és la que fa mal.
  • Es perd la memòria cau i el Location: és un POST opac a un endpoint artificial.
  • Idempotència complicada: reintentar el lot després d'una fallada parcial requereix clau d'idempotència i saber què s'ha aplicat.
  • Timeouts i límits: un lot de 10.000 elements tomba la petició; cal fixar un màxim (Botiga Aroma: 100 operacions) i respondre 413 si se supera.
  • Verb encobert: actualitzacions-lot és un recurs inventat que no existeix al domini. És una concessió conscient, no un patró per estendre.

Decisió de la Botiga Aroma: no hi ha endpoints de lot a la v1. El panell intern actualitza en paral·lel amb peticions individuals. Si el volum ho exigeix, s'afegirà un sol endpoint de lot per a l'estoc, amb les regles anteriors escrites a la guia d'estil.

Errors Comuns i Consells

  • Fer servir GET per a operacions que modifiquen. GET /esborrar?id=res_101 és un desastre esperant un rastrejador. Mai.
  • Fer servir POST per a tot. Funciona, però renuncies a la memòria cau, als reintents segurs i a la meitat de la semàntica d'HTTP: és el nivell 1 de Richardson que ja vam rebutjar a 01-05.
  • Enviar un PUT parcial. L'error clàssic que esborra mig recurs. Si has d'enviar tres camps, fes servir PATCH.
  • Retornar 200 en crear. La creació és 201 amb Location. Un 200 obliga el client a rebuscar l'id al cos.
  • Implementar PATCH sense decidir el format. Sense Content-Type explícit, cada client suposarà una cosa diferent. Documenta application/merge-patch+json i rebutja la resta amb 415.
  • Creure que idempotència és "retorna el mateix". És "deixa el servidor igual". Un segon DELETE pot respondre 404 i continuar sent idempotent.
  • Posar cos a GET o a DELETE. Alguns intermediaris el descarten silenciosament i depurar-ho és un malson.
  • Consell: pregunta't sempre "què passa si això s'envia dues vegades?". Aplica-ho a cada endpoint nou abans de donar-lo per tancat. És la pregunta que més incidents evita.
  • Consell: la clau d'idempotència la genera el client abans d'enviar, no després. Si es genera al reintent, ja no serveix de res.

Exercicis

Exercici 1: triar el mètode

Indica mètode, URI i per què, per a cada necessitat de la Botiga Aroma:

  1. Aroma Mòbil vol saber si la factura de com_5001 ja està disponible, sense descarregar-la.
  2. El panell corregeix una errada al nom de caf_002.
  3. La SPA fixa a 3 unitats la quantitat de caf_002 a la cistella cis_77.
  4. Un moderador rebutja la ressenya res_102 indicant-ne el motiu.
  5. RàpidEnviaments comunica que com_5001 ha sortit a repartiment.
  6. El panell retira caf_001 del catàleg conservant-ne l'històric.
  7. Un client confirma la seva cistella i crea una comanda.

Exercici 2: PUT davant de PATCH

Aquest és l'estat actual de caf_001:

{
  "id": "caf_001",
  "nom": "Etiòpia Yirgacheffe",
  "origen": "Etiòpia",
  "torrefaccio": "clar",
  "preuEuros": 14.50,
  "estoc": 120,
  "notesTast": ["cítric", "floral", "te negre"]
}

a) Què queda després d'un PUT /v1/cafes/caf_001 amb cos {"preuEuros": 15.20}, si el servidor no valida camps obligatoris? b) Escriu el PATCH amb Merge Patch que apugi el preu a 15,20 € i abaixi l'estoc a 95. c) Escriu el PATCH amb Merge Patch que elimini el camp notesTast. d) Escriu el JSON Patch que afegeixi la nota "bergamota" només si l'estoc continua sent 120, i explica per què això no es pot fer amb Merge Patch.

Exercici 3: dissenyar la idempotència d'una devolució

La Botiga Aroma afegeix POST /v1/comandes/{id}/devolucio, que genera una etiqueta de retorn i reemborsa l'import. Dissenya'n el comportament responent:

  1. Ha d'exigir Idempotency-Key? Per què?
  2. Què passa si arriba dues vegades la mateixa clau amb el mateix cos?
  3. Què passa si arriba una devolució d'una comanda que encara no s'ha enviat?
  4. Què passa si arriba una segona devolució, amb clau diferent, d'una comanda ja retornada?
  5. Seria idempotent modelar-ho com a PUT /v1/comandes/{id}/devolucio? Què s'hi guanyaria i què s'hi perdria?

Solucions

Solució 1

# Mètode i URI Justificació
1 HEAD /v1/comandes/com_5001/factura Comprova existència i mida sense gastar dades: exactament per a això existeix HEAD
2 PATCH /v1/cafes/caf_002 amb {"nom": "..."} Canvi parcial; amb PUT caldria reenviar tota la fitxa i arriscar-se a trepitjar altres camps
3 PUT /v1/cistelles/cis_77/linies/caf_002 amb {"quantitat": 3} "Fixar la quantitat" és substitució i idempotent; amb POST se sumaria cada vegada
4 POST /v1/ressenyes/res_102/rebuig amb {"motiu": "..."} Acció amb paràmetres propis, modelada com a subrecurs (02-02)
5 PUT /v1/comandes/com_5001/enviament amb l'estat complet Singleton actualitzat per un soci que reintenta: la idempotència de PUT és imprescindible
6 DELETE /v1/cafes/caf_001 Esborrat lògic per dins; des de fora el cafè desapareix del catàleg
7 POST /v1/comandes amb Idempotency-Key Creació a la col·lecció, no idempotent per naturalesa: la clau evita comandes duplicades

Solució 2

a) Un PUT literal substitueix el recurs complet, així que quedaria:

{ "id": "caf_001", "preuEuros": 15.20 }

Sense nom, sense origen, sense torrefacció, sense estoc i sense notes de tast. L'id sobreviu perquè forma part de la identitat, no del contingut enviat. Per això la Botiga Aroma valida els camps obligatoris i retorna 400 dades_invalides en lloc de destruir el recurs.

b)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "preuEuros": 15.20, "estoc": 95 }

c)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "notesTast": null }

d)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "test", "path": "/estoc", "value": 120 },
  { "op": "add",  "path": "/notesTast/-", "value": "bergamota" }
]

Amb Merge Patch és impossible per dues raons acumulades: no existeix cap operació condicional (test), i els arrays se substitueixen sencers, així que "afegir al final" obliga a enviar la llista completa —cosa que, a més, trepitjaria qualsevol nota afegida per un altre usuari entre la lectura i l'escriptura. L'alternativa de la Botiga Aroma per al cas condicional no és JSON Patch, sinó If-Match amb ETag (04-06).

Solució 3

  1. Sí, obligatòria. Mou diners: un reintent per timeout no pot provocar dos reemborsaments. Mateix criteri que /pagament.
  2. No es reemborsa dues vegades. El servidor retorna la resposta desada de la primera execució amb Idempotent-Replay: true i el mateix 201 i Location.
  3. 409 Conflict amb un codi de negoci nou, comanda_no_enviada: la comanda existeix i la petició està ben formada, però el seu estat actual no permet la transició. No és 400 (les dades són vàlides) ni 404 (la comanda existeix).
  4. 409 Conflict amb comanda_ja_retornada. La clau nova no ajuda: és la màquina d'estats del recurs la que rebutja la segona transició. És l'exemple de per què calen les dues defenses de la secció 10.
  5. Sí que seria idempotent, i aquest és el seu atractiu: PUT /devolucio significaria "vull que existeixi aquesta devolució amb aquestes dades", i repetir-ho deixaria el mateix estat. S'hi guanyaria idempotència sense capçalera addicional. S'hi perdria, en canvi, la semàntica d'acció amb efectes (un PUT suggereix que s'escriu una dada, no que s'executa un reemborsament), la possibilitat que el servidor assigni dades pròpies de l'operació (referència del reemborsament, data) i la coherència amb /pagament, /anullacio i /aprovacio, que ja fan servir POST. La Botiga Aroma prioritza la coherència: POST amb clau d'idempotència.

Conclusió

Els mètodes HTTP són el vocabulari de verbs de la teva API i fer-los servir bé és el que separa el nivell 1 del nivell 2 de Richardson. GET llegeix sense efectes i es pot posar a la memòria cau; POST crea i executa accions, i no és idempotent; PUT substitueix del tot i sí que ho és; PATCH modifica just allò necessari —a la Botiga Aroma amb JSON Merge Patch i Content-Type: application/merge-patch+json—; DELETE elimina, encara que per dins sigui un esborrat lògic; i HEAD i OPTIONS donen informació sense transferir el recurs. Per damunt de tot queden la seguretat i la idempotència, que deixen de ser teoria així que hi ha reintents, timeouts o un doble clic: per això el pagament i la creació de comandes exigeixen Idempotency-Key i, a més, la màquina d'estats del recurs rebutja les transicions impossibles.

Justament aquí hem anat deixant caps solts: 201 amb Location, 204 sense cos, 404 davant de 410, 409 quan la transició no és possible, 415 quan el Content-Type no s'admet, 405 quan el mètode no està permès. Cadascun d'aquests números és una decisió de contracte. A la lliçó següent, 02-04 Codis d'estat HTTP, els recorrerem tots amb criteri, construirem un arbre de decisió per triar el correcte, dissenyarem el cos de l'error de la Botiga Aroma davant de l'estàndard application/problem+json i tancarem el catàleg de codis d'error de negoci.

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