Ja està dissenyat el sobre: sabem a quina URI s'adreça la petició, amb quin mètode i amb quin codi respon el servidor. Falta allò que va a dins. Aquesta lliçó dissenya el cos de les respostes de la Botiga Aroma —els noms dels camps, els seus tipus, com es representen dates i imports, què s'incrusta i què s'enllaça— i les capçaleres que governen aquest cos. És una lliçó de decisions petites i molt duradores: el nom d'un camp publicat és contracte durant anys, i equivocar-se amb els imports monetaris es paga en cèntims perduts. En acabar tindràs la representació canònica de cada recurs i les regles de negociació de contingut que el mòdul 3 implementarà.
Contingut
- Què és una representació
- Convencions de noms i tipus
- Nuls davant de camps absents
- Dates, hores i zones horàries
- Imports monetaris
- Enumerats i booleans
- Embolcall o objecte nu
- Relacions: incrustar o enllaçar
- Hipermèdia selectiva: el disseny de
_links - Expansió i selecció de camps
- Negociació de contingut
- Respostes que no són JSON
- Què és una representació
Recordem la distinció de 01-04: el recurs és l'entitat conceptual (el cafè caf_001), i la representació és una de les seves formes concretes en un moment donat. El mateix recurs es pot representar com a JSON en català, JSON en castellà, PDF o imatge JPEG, i totes comparteixen URI.
graph LR
R["Recurs<br/><b>/v1/comandes/com_5001/factura</b>"] --> A["Accept: application/json<br/>→ JSON amb els imports"]
R --> B["Accept: application/pdf<br/>→ PDF amb capçalera impresa"]
R --> C["Accept-Language: es<br/>→ mateix contingut en castellà"]
D'aquí la regla, ja enunciada a 02-02: el format no va a la URL, es negocia amb capçaleres. I d'aquí també que les decisions d'aquesta lliçó siguin tan importants: la representació és allò que el consumidor veu realment i contra el qual programa.
- Convencions de noms i tipus
2.1. camelCase
La Botiga Aroma fa servir camelCase a tots els noms de camp JSON: preuEuros, dataCreacio, notesTast, clientId.
| Estil | Exemple | Qui el fa servir | Comentari |
|---|---|---|---|
camelCase |
preuEuros |
Google, Stripe (parcial), la majoria | Natural en JavaScript, que és el consumidor principal |
snake_case |
preu_euros |
Stripe, Twitter/X, Slack | Natural en Python/Ruby, llegible |
PascalCase |
PreuEuros |
APIs .NET antigues | Poc freqüent avui |
kebab-case |
preu-euros |
Pràcticament ningú | Incòmode: obliga a obj["preu-euros"] |
Cap no és millor en abstracte. La Botiga Aroma tria camelCase perquè els seus consumidors principals són JavaScript (SPA, Aroma Mòbil amb React Native, servidor Node.js) i així l'objecte JSON es fa servir sense traducció. El que sí que és obligatori és no barrejar: {"preuEuros": 14.50, "data_creacio": "..."} és el tipus de detall que enverina una API.
2.2. Regles d'anomenament de camps
- Sense prefixos tècnics ni abreviatures críptiques:
nom, nostrNomninm. - Sufix
Idper a referències:clientId,cafeId. Deixa clar que és una referència i no l'objecte. - Sufix d'unitat quan n'hi hagi:
preuEuros,pesGrams,duracioSegons. Elimina d'un cop la pregunta "això en quina unitat està?". - Plural per als arrays:
notesTast,linies,dades. - Sense
is/hasen català: el booleà es diuactiu,esgotat,regal. - Noms de domini, no de taula:
origen, nofkOrigen.
2.3. Tipus
| Tipus JSON | Ús a la Botiga Aroma | Exemple |
|---|---|---|
string |
Text, identificadors, dates, enumerats | "caf_001" |
number |
Quantitats i imports | 14.50, 120 |
boolean |
Banderes | true |
array |
Col·leccions i llistes de valors | ["cítric", "floral"] |
object |
Estructures imbricades | {"cafeId": "...", "quantitat": 2} |
null |
Absència amb significat | "dataEnviament": null |
Dues regles dures:
- El tipus d'un camp no canvia mai. Si
estocés un nombre, no pot tornar-se"120"en una altra resposta. És un canvi trencador (02-07) i una font inexhaurible de bugs, perquè"0"és cert en JavaScript i0és fals. - Els identificadors són sempre
string. Encara que5001sembli un nombre,com_5001és una cadena opaca, i així queda protegit el dia que canviï el format.
- Nuls davant de camps absents
Tres estats possibles per a un camp, i cal triar què significa cadascun:
| Forma | Significat que li donem | Exemple |
|---|---|---|
| Camp present amb valor | Hi ha dada | "dataEnviament": "2026-03-16T09:00:00Z" |
Camp present amb null |
La dada existeix conceptualment però encara no té valor | "dataEnviament": null (comanda sense enviar) |
| Camp absent | El camp no aplica a aquest recurs o no s'ha demanat | Sense dataEnviament en una comanda anul·lada |
Regla de la Botiga Aroma: la representació d'un recurs inclou sempre els mateixos camps, fent servir null per a allò que encara no té valor. Les úniques absències legítimes són (a) els camps que el client ha exclòs amb camps= i (b) els objectes que no s'han demanat amb expandir=.
Per què val la pena aquesta regla:
// Amb la regla: el client escriu això i funciona sempre
const data = comanda.dataEnviament ?? "Pendent d'enviament";
// Sense la regla, el client s'ha de defensar de tres casos
const data = ("dataEnviament" in comanda)
? (comanda.dataEnviament === null ? "Pendent" : comanda.dataEnviament)
: "Desconegut";Dos matisos:
- En arrays no es fa servir
null: una llista sense elements és[], mainull. Així el client la pot recórrer sense comprovar res. - Al
PATCHamb Merge Patch,nullsignifica "esborra" (02-03). És una asimetria deliberada entre entrada i sortida, i cal documentar-la.
- Dates, hores i zones horàries
Totes les dates i hores van en ISO-8601 amb zona horària explícita, en UTC (Z).
{
"dataCreacio": "2026-03-14T10:30:00Z",
"dataPagament": "2026-03-14T10:32:15Z",
"dataEnviament": null,
"dataCaducitat": "2027-01-31"
}| Format | Exemple | Veredicte |
|---|---|---|
ISO-8601 amb Z |
2026-03-14T10:30:00Z |
✅ L'estàndard de la Botiga Aroma |
| ISO-8601 amb desplaçament | 2026-03-14T11:30:00+01:00 |
✅ S'accepta a l'entrada, es normalitza a UTC |
| ISO-8601 sense zona | 2026-03-14T10:30:00 |
❌ Ambigu: l'hora de qui? |
| Només data | 2027-01-31 |
✅ Només quan l'hora no aplica |
| Epoch en segons | 1773484200 |
❌ Il·legible; ambigüitat segons/mil·lisegons |
| Format local | 14/03/2026 11:30 |
❌ Ambigu (març o el dia 3?) i dependent de l'idioma |
Punts importants:
- UTC a l'emmagatzematge i al transport; hora local només a la presentació. El client formata segons la zona de l'usuari; el servidor mai no suposa Barcelona.
- Compte amb l'horari d'estiu. Aquí es canvia de
+01:00a+02:00; si guardes hora local, dues comandes de la matinada del canvi poden aparèixer desordenades o duplicades. - Dates sense hora (
2027-01-31) per a allò que és un dia natural, com la caducitat d'un lot. Posar-hiT00:00:00Zconvida a errors d'un dia per desplaçament de zona. - Noms coherents:
data<Alguna cosa>per a instants,<alguna cosa>Diesper a durades. La Botiga Aroma evitatimestampa seques.
- Imports monetaris
L'apartat on més diners es perden per un descuit tècnic. Mai no facis servir coma flotant binària per a diners al servidor.
// El clàssic que sorprèn tothom
0.1 + 0.2 // 0.30000000000000004
14.50 * 3 // 43.5 (bé)
0.07 * 100 // 7.000000000000001
(29.00 * 0.21).toFixed(2) // "6.09" ... de vegadesEl number de JSON, quan es processa com a double d'IEEE 754, no pot representar exactament 0.1. Sumar cent línies de comanda acumula error, i en comptabilitat un cèntim de descompensació és un problema real.
Opcions de representació:
| Opció | Exemple | A favor | En contra |
|---|---|---|---|
| Nombre decimal | "preuEuros": 14.50 |
Llegible, còmode per al client | Risc de coma flotant si el client calcula |
| Enter en cèntims | "preuCentims": 1450 |
Exacte, sense decimals | Tothom ha de saber l'escala; lleig de llegir |
| Cadena decimal | "preuEuros": "14.50" |
Exacte i sense ambigüitat | Obliga a parsejar; incòmode per ordenar |
| Objecte import | {"import": "14.50", "moneda": "EUR"} |
Explícit, multidivisa | Verbós |
Decisió de la Botiga Aroma:
- A la representació: nombre amb exactament dos decimals i sufix
Euros(14.50,29.00). És llegible i directe per als clients, que només el mostren. - Al servidor i a la base de dades: enters de cèntims o tipus decimal exacte. La conversió passa a la vora (03-05).
- Els totals els calcula sempre el servidor. El client mai no suma imports per mostrar-los com a oficials: per això la comanda inclou
totalEurosja calculat. - Moneda: la v1 és només euros i així consta a la documentació. Si algun dia hi ha més divises, s'hi afegeix un camp
monedaopcional amb valor per defecte"EUR"—canvi retrocompatible (02-07)— en lloc de reestructurar els imports.
Detall que sorprèn: 29.00 en JSON es pot serialitzar com a 29 segons la biblioteca, perquè JSON no distingeix enters de decimals. És acceptable —numèricament són iguals— i el client formata amb dos decimals en mostrar-ho. Si et molesta, l'alternativa és la cadena decimal, amb el seu cost.
- Enumerats i booleans
6.1. Enumerats
Els valors enumerats de la Botiga Aroma van en snake_case en minúscules, amb valors estables i ampliables:
| Camp | Valors v1 |
|---|---|
torrefaccio |
clar, mitja, fosc |
estat (comanda) |
pendent_pagament, pagat, enviat |
estat (ressenya) |
pendent_moderacio, publicada, rebutjada |
estat (enviament) |
pendent_recollida, en_repartiment, lliurat |
Regles del contracte:
- El valor no es tradueix mai.
estat: "pagat"és un identificador de màquina; l'etiqueta que veu l'usuari la posa el client. Si demà cal un text llegible, s'hi afegeix un camp a part (estatText), no es canvia el valor. - La llista pot créixer. La documentació adverteix des del primer dia: tracta un valor desconegut amb elegància (principi de robustesa, 02-01). Afegir
estat: "retornat"no ha de trencar res a ningú. - La llista no encongeix i els valors no es reanomenen. Això sí que és trencador.
- Res de codis numèrics.
torrefaccio: 1obliga a mantenir una taula de correspondències fora de banda i és il·legible en un log.
6.2. Booleans
Un booleà només s'ha de fer servir quan el concepte sigui genuïnament binari i per sempre. Molts "booleans" acaben convertits en enumerats: aprovada: true/false no cobreix "pendent de moderació", que és exactament per què les ressenyes tenen estat i no aprovada. Davant del dubte, enumerat: ampliar un enumerat és retrocompatible; convertir un booleà en enumerat, no.
- Embolcall o objecte nu
Per a una col·lecció, què es retorna?
Opció A, array nu:
Opció B, embolcall (Botiga Aroma):
{
"dades": [
{ "id": "caf_001", "nom": "Etiòpia Yirgacheffe" },
{ "id": "caf_002", "nom": "Colòmbia Huila" }
],
"total": 2
}| Criteri | Array nu | Embolcall dades/total |
|---|---|---|
| Simplicitat per al client | Màxima: s'itera directament | Un nivell més (resposta.dades) |
| Afegir metadades després | Canvi trencador | Additiu, sense trencar res |
| Total d'elements | No hi cap (o va en capçalera) | total |
| Coherència amb les respostes d'element | Dues formes diferents | Dues formes diferents igualment |
| Risc històric de JSON hijacking | Existia en navegadors antics | Mitigat |
Decisió de la Botiga Aroma: embolcall per a col·leccions, objecte nu per a elements.
GET /v1/cafes/caf_001 → { "id": "caf_001", "nom": "...", ... }
GET /v1/cafes → { "dades": [...], "total": 137 }La raó principal és la tolerància a l'evolució: si demà cal afegir total, _links o avisos de depreciació a una col·lecció, amb l'embolcall és additiu i amb l'array nu caldria trencar tots els clients. I no emboliquem els elements individuals ({"dades": {...}}) perquè afegeix soroll sense aportar res: un element ja és un objecte extensible.
Conseqüència pràctica per al client, que cal documentar bé:
// Col·lecció
const resposta = await fetch("/v1/cafes").then(r => r.json());
resposta.dades.forEach(cafe => console.log(cafe.nom));
console.log(`Hi ha ${resposta.total} cafès en total`);
// Element
const cafe = await fetch("/v1/cafes/caf_001").then(r => r.json());
console.log(cafe.nom);Les metadades de paginació que acompanyen total es decideixen a 02-06.
- Relacions: incrustar o enllaçar
Una comanda té un client i línies que apunten a cafès. Quant d'això viatja a la resposta?
Enllaçar (linking):
{
"id": "com_5001",
"clientId": "cli_842",
"totalEuros": 29.00,
"_links": {
"self": { "href": "/v1/comandes/com_5001" },
"client": { "href": "/v1/clients/cli_842" }
}
}Incrustar (embedding):
{
"id": "com_5001",
"client": {
"id": "cli_842",
"nom": "Marta Garcia",
"email": "[email protected]"
},
"totalEuros": 29.00
}| Criteri | Enllaçar | Incrustar |
|---|---|---|
| Mida de la resposta | Mínima | Més gran |
| Nombre de crides del client | Més (chattiness) | Menys |
| Frescor de la dada | Sempre actual en demanar-la | Còpia de l'instant de la resposta |
| Memòria cau | Cada recurs es guarda a part | Invalida tot el conjunt |
| Risc d'exposar de més | Baix | Alt (dades personals, permisos) |
| Acoblament | Baix | Alt |
Criteri de la Botiga Aroma:
- S'incrusta allò que gairebé sempre es necessita i és petit i estable. El
nomdel cafè i elpreuEurosde cada línia de comanda s'incrusten, i a més el preu s'incrusta congelat: el preu de la comanda és el que hi havia el dia de la compra, no l'actual. Aquí incrustar no és una optimització, és correcció de negoci. - S'enllaça allò gran, canviant o sensible. El client complet, les ressenyes d'un cafè, la factura.
- Mai no s'incrusta una col·lecció sense límit. Un cafè amb 4.000 ressenyes no les pot portar a dins: van enllaçades i paginades.
- La resta, sota demanda amb
expandir(secció 10).
Així queda una comanda de la Botiga Aroma:
{
"id": "com_5001",
"clientId": "cli_842",
"estat": "pendent_pagament",
"totalEuros": 29.00,
"dataCreacio": "2026-03-14T10:30:00Z",
"dataPagament": null,
"dataEnviament": null,
"linies": [
{ "cafeId": "caf_001", "nom": "Etiòpia Yirgacheffe", "quantitat": 2, "preuEuros": 14.50 }
],
"_links": {
"self": { "href": "/v1/comandes/com_5001" },
"client": { "href": "/v1/clients/cli_842" },
"pagar": { "href": "/v1/comandes/com_5001/pagament", "method": "POST" },
"anullar": { "href": "/v1/comandes/com_5001/anullacio", "method": "POST" }
}
}
- Hipermèdia selectiva: el disseny de
_links
_linksA 01-05 vam fixar el nivell objectiu: Richardson 2 sòlid amb hipermèdia selectiva. Concretem què significa exactament al contracte, perquè "selectiva" sense regles es converteix en caos.
Què SÍ que porta enllaços:
| Element | Enllaços | Motiu |
|---|---|---|
| Tot recurs individual | self |
URI canònica, imprescindible amb vistes imbricades (02-02) |
| Tot recurs amb relacions | Enllaços als recursos relacionats | Evita que el client construeixi URLs |
| Només les comandes | Enllaços d'acció segons l'estat | La màquina d'estats és real i canvia |
| Col·leccions | Paginació mitjançant la capçalera Link |
Es decideix a 02-06 |
Què NO porta enllaços: els elements dins d'una col·lecció no porten _links complets (només self), per no multiplicar el pes de la resposta per vint; els cafès no porten enllaços d'acció, perquè no tenen màquina d'estats.
Format de l'enllaç. Objecte amb href i, a les accions, method:
"_links": {
"self": { "href": "/v1/comandes/com_5001" },
"pagar": { "href": "/v1/comandes/com_5001/pagament", "method": "POST" }
}És la forma de HAL simplificada (01-05), però sense adoptar application/hal+json ni _embedded: continuem amb application/json, perquè no volem obligar els clients a entendre un format hipermèdia complet.
Els enllaços d'acció segons l'estat de la comanda són el cor de la hipermèdia selectiva:
| Estat | _links presents |
|---|---|
pendent_pagament |
self, client, pagar, anullar |
pagat |
self, client, factura, enviament, anullar |
enviat |
self, client, factura, enviament, retornar |
I la regla que fa que això serveixi d'alguna cosa, escrita a la documentació: "si un enllaç d'acció no hi és present, aquesta acció no és possible ara; no la construeixis a mà". La SPA pinta els botons a partir de _links en comptes de replicar la màquina d'estats, que és exactament el que buscàvem.
Les URIs dels enllaços són relatives a l'amfitrió (/v1/comandes/com_5001). Es documenta així perquè funcioni igual en producció, en preproducció i en local.
- Expansió i selecció de camps
Les dues vàlvules anunciades a 02-01 per governar la granularitat.
10.1. Expansió (expandir)
Incrusta sota demanda un recurs relacionat, estalviant crides:
{
"id": "com_5001",
"clientId": "cli_842",
"client": {
"id": "cli_842",
"nom": "Marta Garcia",
"email": "[email protected]"
},
"totalEuros": 29.00
}Regles de la Botiga Aroma per a expandir:
- Llista separada per comes:
?expandir=client,linies.cafe. - S'admet un sol nivell de profunditat (
linies.cafesí;linies.cafe.ressenyesno) per evitar consultes incontrolables. - El camp original es manté:
clientIdno desapareix en afegir-seclient. Així el client no ha d'escriure dues rutes d'accés diferents. - Només es poden expandir les relacions documentades; un valor desconegut retorna
400ambparametre_invalid. - Màxim tres expansions per petició.
10.2. Selecció de camps (camps)
També anomenada sparse fieldsets: el client demana només allò que farà servir.
{
"dades": [
{ "id": "caf_001", "nom": "Etiòpia Yirgacheffe", "preuEuros": 14.50 },
{ "id": "caf_002", "nom": "Colòmbia Huila", "preuEuros": 12.90 }
],
"total": 137
}Regles:
ids'inclou sempre, es demani o no: sense ell la resposta és inservible.- Els camps no demanats s'ometen (única excepció legítima a la regla de la secció 3).
- Un camp desconegut retorna
400ambparametre_invalid, en lloc d'ignorar-se en silenci: així es detecten les errades. - No es combina amb
expandirsobre la mateixa relació a la v1, per no multiplicar casos.
Això connecta directament amb l'over-fetching que vam discutir a 01-07 en comparar REST amb GraphQL: camps i expandir cobreixen el 90 % de la necessitat real sense renunciar a la memòria cau HTTP ni assumir la complexitat d'un llenguatge de consultes. És la resposta d'una API REST ben dissenyada a aquest argument.
I el cost, que cal assumir amb els ulls oberts: cada combinació de paràmetres és una URL diferent i, per tant, una entrada de memòria cau diferent. Multiplicar variants redueix la taxa d'encerts de la memòria cau (04-06).
- Negociació de contingut
És el mecanisme pel qual client i servidor acorden la representació. El client proposa amb capçaleres Accept-*, el servidor tria i ho declara amb Content-*.
| Capçalera del client | Què negocia | Capçalera de resposta | Error si no hi ha acord |
|---|---|---|---|
Accept |
Format | Content-Type |
406 Not Acceptable |
Accept-Language |
Idioma | Content-Language |
406 (o idioma per defecte) |
Accept-Encoding |
Compressió | Content-Encoding |
Se serveix sense comprimir |
Accept-Charset |
Joc de caràcters | (dins de Content-Type) |
En desús: avui tot és UTF-8 |
Content-Type (petició) |
Format d'allò que envia | — | 415 Unsupported Media Type |
11.1. Factors de qualitat (q=)
El client pot expressar preferències ponderades, de 0 a 1 (per defecte, 1):
Accept: application/json;q=1.0, application/xml;q=0.8, */*;q=0.1
Accept-Language: ca;q=1.0, es;q=0.8, en;q=0.5Es llegeix: "prefereixo JSON; si no, XML; en darrer cas, qualsevol cosa" i "prefereixo català; si no, castellà; si no, anglès". El servidor recorre les opcions per q descendent i serveix la primera que pot produir.
11.2. Què negocia la Botiga Aroma
GET /v1/cafes/caf_001 HTTP/1.1
Accept: application/json
Accept-Language: ca, es;q=0.8
Accept-Encoding: gzip, brHTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: ca
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding
{
"id": "caf_001",
"nom": "Etiòpia Yirgacheffe",
"origen": "Etiòpia",
"torrefaccio": "clar",
"preuEuros": 14.50,
"notesTast": ["cítric", "floral", "te negre"]
}Fixa't en tres coses:
notesTastve traduït, perquè és text de màrqueting pensat per a l'usuari final.torrefacciono es tradueix: és un enumerat, un identificador de màquina (secció 6).Content-Language: cadeclara què s'ha servit; és imprescindible, perquè el client va demanar dos idiomes i necessita saber quin ha rebut.Varydiu a les memòries cau intermèdies que la resposta depèn d'aquestes capçaleres i que no han de servir la versió catalana a qui demani castellà. OblidarVaryés una fallada greu i subtil: es detalla a 04-06.
Contracte d'idiomes de la Botiga Aroma: se serveixen es, ca i en; l'idioma per defecte és ca; si es demana un idioma no disponible, no es retorna 406, se serveix ca i es declara Content-Language: ca. És una decisió pragmàtica: per al contingut, un idioma alternatiu és millor que un error.
11.3. Compressió
Accept-Encoding: gzip, br permet comprimir. Un catàleg de 137 cafès en JSON pot passar de 180 KB a uns 15 KB amb gzip: és l'optimització amb millor relació cost/benefici de tota l'API. S'activa al servidor o a la passarel·la i no canvia el contracte. Detalls de rendiment, a 04-06.
11.4. Media types específics i versionat per media type
A més d'application/json, es poden definir tipus propis que identifiquin la forma exacta de la representació:
El prefix vnd. marca els tipus de proveïdor. El segon exemple és el versionat per media type, una de les estratègies que compararem a 02-07. La Botiga Aroma no el fa servir —versiona a la ruta—, però convé reconèixer-lo: en demanar contingut a una API que versiona així, Accept: application/json et donarà la versió que el servidor consideri per defecte, que pot no ser la que esperes.
- Respostes que no són JSON
No tot és JSON, i la negociació de contingut és justament el que permet conviure sense embrutar les URIs.
12.1. La factura en PDF
El mateix recurs, dues representacions:
# Representació JSON: dades estructurades
curl -H "Accept: application/json" \
https://api.botigaaroma.example/v1/comandes/com_5001/factura{
"id": "fac_88",
"comandaId": "com_5001",
"numeroFactura": "2026/000188",
"dataEmissio": "2026-03-14T10:33:00Z",
"baseImposableEuros": 23.97,
"ivaEuros": 5.03,
"totalEuros": 29.00,
"_links": { "self": { "href": "/v1/comandes/com_5001/factura" } }
}# Representació PDF: document per imprimir o arxivar
curl -H "Accept: application/pdf" -o factura.pdf \
https://api.botigaaroma.example/v1/comandes/com_5001/facturaHTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="factura-2026-000188.pdf"
Content-Length: 48213
Accept-Ranges: bytesContent-Disposition suggereix el nom del fitxer en descarregar, i Accept-Ranges anuncia que s'admeten descàrregues parcials, que és el que habilita el 206 de 02-04. I no hi ha cap URL amb .pdf: és el mateix recurs.
12.2. Pujada de la imatge d'un cafè
Aquí qui envia una cosa que no és JSON és el client. Dos enfocaments:
a) Binari directe amb PUT sobre el singleton /cafes/{id}/imatge:
curl -i -X PUT "https://api.botigaaroma.example/v1/cafes/caf_001/imatge" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: image/jpeg" \
--data-binary @yirgacheffe.jpgHTTP/1.1 200 OK
Content-Type: application/json
Location: https://api.botigaaroma.example/v1/cafes/caf_001/imatge
{
"url": "https://cdn.botigaaroma.example/cafes/caf_001.jpg",
"amplePx": 1200, "altPx": 1200, "bytes": 184320, "format": "image/jpeg"
}És net, idempotent i no necessita cap format addicional.
b) multipart/form-data quan cal enviar fitxer i metadades alhora:
curl -i -X POST "https://api.botigaaroma.example/v1/cafes/caf_001/imatges" \
-H "Authorization: Bearer <token>" \
-F "[email protected];type=image/jpeg" \
-F "descripcio=Gra torrat, pla zenital"Decisió de la Botiga Aroma: l'opció (a), perquè a la v1 només hi ha una imatge principal per cafè i la descripció és un camp del mateix cafè. Contracte de la pujada:
- Formats admesos:
image/jpeg,image/png,image/webp. Qualsevol altre →415. - Mida màxima: 5 MB. Si se supera →
413ambcos_massa_gran. - La resposta és JSON, encara que la petició fos binària: la resposta descriu el recurs creat, no el retorna.
- La imatge se serveix des de la CDN, no des de l'API. L'API la desa i en retorna la URL.
12.3. Altres formats que apareixeran
- CSV per a exportacions del panell intern:
Accept: text/csvsobre/v1/comandes. text/event-streamper al SSE del panell en directe que vam decidir a 01-07: és un altre tipus de resposta negociada, amb connexió persistent.
En tots dos casos la regla és la mateixa: el format es negocia, la URI no canvia.
Errors Comuns i Consells
- Barrejar convencions de noms. Un
camelCaseamb dos camps ensnake_casecola a la revisió i després no es pot treure sense trencar clients. - Retornar dates sense zona horària.
"2026-03-14T10:30:00"és ambigu; el bug apareix al març i a l'octubre, amb el canvi d'hora. - Calcular diners amb coma flotant. Guarda cèntims o decimals exactes i converteix a la vora.
- Traduir els enumerats. Que
estatvalgui"pagado"o"paid"segons l'idioma obliga els clients a mantenir taules per idioma. Els valors de màquina no es tradueixen. - Retornar un array nu a les col·leccions. Et quedes sense lloc per posar metadades sense trencar el contracte.
- Incrustar objectes grans "perquè és còmode". La resposta es dispara, la memòria cau es degrada i acabes exposant dades personals on no tocava.
- Oblidar
Vary. Amb memòria cau intermèdia, un usuari pot rebre la resposta en l'idioma d'un altre. És la fallada més difícil de reproduir d'aquesta lliçó. - Posar el format a la URL.
.json/.pdfdupliquen identitats; per a això hi haAccept. - Consell: escriu primer el JSON ideal a mà. Abans de mirar el model de dades, escriu la resposta que voldries rebre. És la millor defensa contra l'abocament de taules.
- Consell: revisa cada camp preguntant "qui el consumeix?". Si no hi ha resposta, treu-lo: cada camp publicat s'ha de mantenir per sempre.
Exercicis
Exercici 1: corregir una representació
Aquest és el JSON que proposa un equip per a una ressenya. Reescriu-lo segons les convencions de la Botiga Aroma i justifica cada canvi.
{
"Id": 101,
"cafe_id": 1,
"user": { "id": 842, "nom": "Marta Garcia", "password_hash": "$2b$10$..." },
"puntuacio": "5",
"comentari": "Un cafè espectacular",
"aprovada": true,
"data": "14/03/2026 11:30",
"preu_pagat": 14.5,
"respostes": null
}Exercici 2: dissenyar la negociació de contingut
Aroma Mòbil, amb l'app en català i en una xarxa lenta, vol el detall de la comanda com_5001 amb les dades del client incloses, però només els camps que pinta a la pantalla (id, estat, totalEuros, dataCreacio).
a) Escriu la petició curl completa amb totes les capçaleres pertinents.
b) Escriu la resposta del servidor amb les seves capçaleres.
c) Explica per què cal Vary i què passaria exactament si s'ometés.
Exercici 3: incrustar o enllaçar
Per a cada relació, decideix si s'incrusta, s'enllaça o s'ofereix amb expandir, i justifica-ho amb els criteris de la secció 8:
- El nom del cafè dins d'una línia de comanda.
- Les 4.000 ressenyes de
caf_001a la fitxa del cafè. - El client complet dins d'una comanda, al panell intern.
- L'adreça d'enviament dins d'una comanda.
- La puntuació mitjana d'un cafè al llistat del catàleg.
- L'historial de pagaments d'un client a la seva fitxa.
Solucions
Solució 1
{
"id": "res_101",
"cafeId": "caf_001",
"clientId": "cli_842",
"puntuacio": 5,
"comentari": "Un cafè espectacular",
"estat": "publicada",
"dataCreacio": "2026-03-14T10:30:00Z",
"respostes": [],
"_links": {
"self": { "href": "/v1/ressenyes/res_101" },
"cafe": { "href": "/v1/cafes/caf_001" },
"respostes": { "href": "/v1/ressenyes/res_101/respostes" }
}
}| Problema | Correcció |
|---|---|
"Id": 101 |
"id": "res_101": minúscula, cadena i amb prefix de tipus |
cafe_id |
cafeId: camelCase coherent amb la resta |
user incrustat |
Se substitueix per clientId + enllaç: l'objecte complet no fa falta aquí |
password_hash |
S'elimina. Fuita gravíssima: mai no es serialitza un camp sensible per incrustar un objecte sencer |
"puntuacio": "5" |
Nombre, no cadena: és una quantitat i s'ordena numèricament |
aprovada: true |
estat: "publicada": el booleà no cobreix pendent_moderacio ni rebutjada |
"data": "14/03/2026 11:30" |
dataCreacio en ISO-8601 UTC; el format local és ambigu i dependent de l'idioma |
preu_pagat |
S'elimina: no pertany a una ressenya; aquesta dada viu a la línia de la comanda |
"respostes": null |
[]: els arrays buits no són null, perquè el client els pugui recórrer sempre |
Solució 2
a)
curl -i "https://api.botigaaroma.example/v1/comandes/com_5001?expandir=client&camps=id,estat,totalEuros,dataCreacio" \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
-H "Accept-Language: ca, es;q=0.8" \
-H "Accept-Encoding: gzip, br"b)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: ca
Content-Encoding: br
Vary: Accept, Accept-Language, Accept-Encoding
{
"id": "com_5001",
"estat": "pendent_pagament",
"totalEuros": 29.00,
"dataCreacio": "2026-03-14T10:30:00Z",
"client": { "id": "cli_842", "nom": "Marta Garcia", "email": "[email protected]" }
}Nota: estat continua sent un identificador de màquina, no text traduïble; l'app el converteix en "Pendent de pagament" en pintar-lo, i seria el mateix valor demanant la resposta en qualsevol altre idioma. I client hi apareix encara que no consti a camps perquè l'expansió és explícita: es documenta així perquè no sorprengui.
c) Vary indica a les memòries cau intermèdies (CDN, proxy corporatiu, passarel·la) quines capçaleres de la petició influeixen en la resposta. Sense ella, la memòria cau desaria aquesta resposta sota la clau "URL" a seques. Conseqüència concreta: el següent usuari que demanés la mateixa comanda amb Accept-Language: es rebria la versió en català emmagatzemada; i un client que no admetés Brotli podria rebre un cos comprimit amb br que no sap descomprimir, amb la qual cosa la resposta seria brossa il·legible.
Solució 3
| # | Decisió | Justificació |
|---|---|---|
| 1 | Incrustar | Petit, sempre necessari i, sobretot, és una còpia històrica: la comanda ha de mostrar el nom i el preu del dia de la compra |
| 2 | Enllaçar | Col·lecció sense límit: mai no s'incrusta. _links.ressenyes apunta a /v1/cafes/caf_001/ressenyes, paginada (02-06) |
| 3 | expandir=client |
El panell el necessita sovint, però incrustar-lo sempre exposaria dades personals a tots els consumidors i engreixaria cada resposta |
| 4 | Incrustar | És part de la comanda i també una dada congelada: l'adreça a la qual es va enviar, encara que el client la canviï després |
| 5 | Incrustar un camp calculat (puntuacioMitjana, nombreRessenyes) |
Són dos nombres, es mostren a cada targeta del catàleg i eviten una crida per cafè: enllaçar aquí seria chattiness pura |
| 6 | Enllaçar | Col·lecció que creix sense límit, amb dades sensibles i d'ús ocasional: /v1/clients/cli_842/pagaments |
Conclusió
La representació és allò que el consumidor veu realment, i ara està dissenyada de dalt a baix: camelCase, identificadors com a cadenes opaques, camps sempre presents amb null per a allò que encara no té valor i [] per a les llistes buides, dates ISO-8601 en UTC, imports en euros amb dos decimals i cèntims exactes per dins, enumerats en snake_case ampliables i sense traduir, i l'embolcall {"dades": [...], "total": n} per a les col·leccions davant de l'objecte nu per als elements. Hem fixat què s'incrusta —allò petit, estable i congelat, com el nom i el preu d'una línia— i què s'enllaça, com són exactament els _links de la hipermèdia selectiva de nivell 2, i com expandir i camps donen a cada consumidor la granularitat que necessita sense renunciar a REST. I hem tancat la negociació de contingut amb Accept, Accept-Language i Accept-Encoding, amb Vary com a peça imprescindible, incloses les representacions que no són JSON: la factura en PDF i la pujada d'imatges.
Queda una peça que hem anat ajornant i que es nota així que el catàleg creix: què passa quan una col·lecció té 4.000 elements. A la lliçó següent, 02-06 Filtratge, ordenació, paginació i cerca, dissenyarem les col·leccions grans de la Botiga Aroma: els convenis de filtres i rangs, l'ordenació estable, els tres models de paginació amb la seva taula comparativa i el problema del deep paging, on viatgen les metadades de paginació —total i capçalera Link—, la cerca de text i els límits per defecte que protegeixen l'API.
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
