Tot el que hem dissenyat fins ara funciona igual de bé amb 5 cafès que amb 5.000… fins que arriba el primer GET /v1/cafes sobre un catàleg real i la resposta pesa 40 MB. Les col·leccions són el punt on una API ben dissenyada es distingeix d'una que aguanta només a l'entorn de desenvolupament. Aquesta lliçó dissenya les col·leccions grans de la Botiga Aroma: com es filtren, com s'ordenen, com es parteixen en pàgines —amb els tres models existents i les seves conseqüències reals— i com s'hi cerca text. És disseny de contracte, no d'implementació: aquí es decideix quins paràmetres existeixen i què prometen, i el mòdul 3 els implementarà tal qual.
Contingut
- Per què una col·lecció sense límits és un problema
- Filtratge: convenis de query params
- Filtres per rang i per múltiples valors
- Per què no cal inventar un llenguatge de consulta a la URL
- Ordenació
- Paginació per offset
- Paginació per pàgina
- Paginació per cursor
- Comparativa i deep paging
- On viatgen les metadades de paginació
- Cerca de text
- Valors per defecte, límits i documentació
- Per què una col·lecció sense límits és un problema
GET /v1/cafes sense restriccions sembla inofensiu. Amb 137 cafès ho és. Amb 50.000 referències, o amb GET /v1/comandes sobre l'històric complet, deixa de ser-ho per quatre motius simultanis:
| Problema | Què passa |
|---|---|
| Base de dades | Un SELECT sense LIMIT recorre i materialitza la taula sencera |
| Memòria del servidor | Serialitzar 50.000 objectes a JSON pot consumir centenars de MB; amb diverses peticions alhora, el procés mor |
| Xarxa i client | 40 MB per una pantalla que mostra 20 files; al mòbil, inacceptable |
| Disponibilitat | Qualsevol pot tombar l'API repetint aquesta crida: és una denegació de servei gratuïta |
L'últim punt és l'important i el que se sol passar per alt: una col·lecció sense límit màxim és un vector d'atac, i no cal mala intenció —n'hi ha prou amb un script d'un soci amb un bucle mal escrit—. Per això la primera decisió no és "com pagino", sinó "la paginació és obligatòria i el servidor la imposa encara que el client no la demani".
- Filtratge: convenis de query params
Com vam fixar a 02-02, els criteris de selecció van a la query string. El conveni base de la Botiga Aroma és el més simple possible: un paràmetre per camp, igualtat exacta.
# Cafès de Colòmbia
curl "https://api.botigaaroma.example/v1/cafes?origen=Col%C3%B2mbia"
# Cafès de Colòmbia amb torrefacció mitjana (els filtres es combinen amb I lògic)
curl "https://api.botigaaroma.example/v1/cafes?origen=Col%C3%B2mbia&torrefaccio=mitja"
# Comandes pagades d'un client
curl "https://api.botigaaroma.example/v1/comandes?clientId=cli_842&estat=pagat"Regles del contracte:
- El nom del paràmetre és el nom del camp a la representació (
origen,torrefaccio,estat,clientId). Previsibilitat: qui ha vist el JSON ja sap filtrar. - Diversos filtres es combinen amb I lògic. Mai amb O; per a això hi ha el multivalor de la secció 3.
- Un filtre desconegut retorna
400ambparametre_invalid. Ignorar-lo en silenci és pitjor: el client creu que ha filtrat i rep tot el catàleg. - Un valor invàlid retorna
400:?torrefaccio=torradissimano és un enumerat vàlid. - Sense resultats és
200amb{"dades": [], "total": 0}, mai404(02-03). - Els valors es codifiquen a la URL:
?origen=Eti%C3%B2pia.
Filtres publicats a la v1 de la Botiga Aroma:
| Col·lecció | Filtres |
|---|---|
/cafes |
origen, torrefaccio, preuMin, preuMax, disponible, q |
/comandes |
clientId, estat, dataDes, dataFins |
/ressenyes |
cafeId, clientId, estat, puntuacioMin |
/clients |
q (nom o correu) |
La llista de filtres és tancada i forma part del contracte. No es filtra per qualsevol camp "perquè l'ORM ho permet": cada filtre publicat s'ha de documentar, validar, provar, indexar i mantenir per sempre.
- Filtres per rang i per múltiples valors
3.1. Rangs
El conveni de la Botiga Aroma són dos paràmetres amb sufix, tots dos inclusius:
# Cafès entre 10 i 15 euros, tots dos inclosos
curl "https://api.botigaaroma.example/v1/cafes?preuMin=10&preuMax=15"
# Comandes de març del 2026
curl "https://api.botigaaroma.example/v1/comandes?dataDes=2026-03-01&dataFins=2026-03-31"
# Ressenyes de 4 estrelles o més
curl "https://api.botigaaroma.example/v1/ressenyes?puntuacioMin=4"Convenció de sufixos: Min/Max per a nombres, Des/Fins per a dates. Els dos extrems són opcionals i independents: ?preuMin=10 significa "de 10 € en amunt".
Alternatives que existeixen i que la Botiga Aroma no fa servir, perquè les reconeguis:
| Estil | Exemple | Comentari |
|---|---|---|
| Sufixos (Botiga Aroma) | ?preuMin=10&preuMax=15 |
Llegible, fàcil de validar i de documentar |
| Operadors al valor | ?preu=gte:10,lte:15 |
Compacte, però cal parsejar el valor |
| Claudàtors | ?preu[gte]=10&preu[lte]=15 |
Estil JSON:API; lleig de codificar a la URL |
| Rang amb guionet | ?preu=10-15 |
Ambigu amb negatius i amb decimals |
3.2. Múltiples valors
Per a "això o allò" sobre el mateix camp, llista separada per comes:
# Cafès de torrefacció clara o mitjana
curl "https://api.botigaaroma.example/v1/cafes?torrefaccio=clar,mitja"
# Comandes pagades o enviades
curl "https://api.botigaaroma.example/v1/comandes?estat=pagat,enviat"L'alternativa —repetir el paràmetre, ?torrefaccio=clar&torrefaccio=mitja— és igual de vàlida i la fan servir moltes APIs, però el seu comportament depèn del framework i de la biblioteca del client (alguns es queden amb el darrer valor). La coma és explícita i no admet interpretacions. Limitació assumida: els valors no poden contenir comes; a la Botiga Aroma només s'admet multivalor en enumerats i identificadors, on això no passa.
Resumint la semàntica completa, que cal documentar de manera explícita:
?torrefaccio=clar,mitja&origen=Colòmbia → (torrefaccio = clar O torrefaccio = mitja) I (origen = Colòmbia)
- Per què no cal inventar un llenguatge de consulta a la URL
Tard o d'hora algú proposarà una cosa així:
GET /v1/cafes?filtre=(origen eq 'Colòmbia' and preu gt 10) or torrefaccio eq 'clar'
GET /v1/cafes?where={"$or":[{"preu":{"$gt":10}},{"torrefaccio":"clar"}]}És temptador: resol qualsevol consulta futura sense tocar l'API. I gairebé sempre és un error:
- Cal escriure un parser i un avaluador, amb els seus errors de sintaxi, els seus missatges i els seus casos límit. És un projecte, no un paràmetre.
- Risc d'injecció: passar l'expressió al motor de dades sense traduir-la amb cura és una via directa a NoSQL injection o SQL injection (04-02).
- Impossible d'acotar: el client pot construir consultes amb un cost arbitrari. Adeu als índexs i a les previsions de capacitat.
- Impossible de documentar bé a OpenAPI: el paràmetre és una cadena lliure, així que no hi ha validació automàtica, ni autocompletat, ni mocks útils (02-08).
- La memòria cau en pateix: infinites combinacions d'URL, cap de reutilitzable.
Hi ha estàndards seriosos per a això —OData i GraphQL (01-07)— i la lliçó és que, si de debò necessites consultes arbitràries, n'adoptes un amb els ulls oberts, no t'inventes un dialecte. La Botiga Aroma es queda amb filtres explícits: cobreixen els casos d'ús reals, es documenten sols i són predictibles en cost. Si apareix una consulta legítima que no hi encaixa, s'hi afegeix un filtre nou (canvi retrocompatible, 02-07) o es crea un recurs específic.
- Ordenació
Un sol paràmetre, ordenar, amb el nom del camp i un guionet al davant per a l'ordre descendent:
# Del més barat al més car
curl "https://api.botigaaroma.example/v1/cafes?ordenar=preuEuros"
# Del més car al més barat
curl "https://api.botigaaroma.example/v1/cafes?ordenar=-preuEuros"
# Comandes més recents primer
curl "https://api.botigaaroma.example/v1/comandes?ordenar=-dataCreacio"
# Ordre múltiple: per torrefacció ascendent i, dins de cada torrefacció, per preu descendent
curl "https://api.botigaaroma.example/v1/cafes?ordenar=torrefaccio,-preuEuros"Detalls del contracte:
- Camps ordenables tancats i documentats per col·lecció.
/cafes:nom,preuEuros,estoc,dataCreacio,puntuacioMitjana. Un camp no permès retorna400ambparametre_invalid. La raó és de rendiment: cada camp ordenable necessita el seu índex. - Ordre per defecte, també documentat:
/cafespernomascendent,/comandesper-dataCreacio,/ressenyesper-dataCreacio. - Ordre múltiple amb comes, de major a menor prioritat.
L'ordre estable, o per què això importa més del que sembla
Un ordre és estable quan dues peticions idèntiques retornen els elements en el mateix ordre. Sona obvi, però no ho és si ordenes per un camp amb valors repetits: la base de dades pot retornar els empats en qualsevol ordre entre una consulta i la següent.
Conseqüència directa i molt real en paginar:
# 40 cafès costen exactament 12,90 €
curl "https://api.botigaaroma.example/v1/cafes?ordenar=preuEuros&limit=20&desplacament=0"
curl "https://api.botigaaroma.example/v1/cafes?ordenar=preuEuros&limit=20&desplacament=20"Si el motor resol els empats de manera diferent a cada consulta, hi ha cafès que apareixen a les dues pàgines i cafès que no apareixen a cap. L'usuari veu duplicats i perd elements, i el bug és intermitent i impossible de reproduir en desenvolupament amb 10 files.
Solució, i és contracte: el servidor sempre afegeix un criteri de desempat únic al final de l'ordre sol·licitat, típicament l'id.
No apareix a la URL, però es documenta: "l'ordre es desempata sempre per id ascendent". Sense això, cap paginació no és fiable.
- Paginació per offset
El model més estès: "salta N elements i dona-me'n M".
# Primera pàgina
curl "https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=0"
# Segona pàgina
curl "https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=20"
# Pàgina 7
curl "https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=120"{
"dades": [ { "id": "caf_001", "nom": "Etiòpia Yirgacheffe", "preuEuros": 14.50 } ],
"total": 137
}A favor: és trivial d'entendre, permet saltar a qualsevol pàgina (desplacament = (pagina - 1) * limit) i dona el total, amb la qual cosa el client pot pintar "137 resultats, pàgina 3 de 7".
En contra, dos problemes seriosos que veurem a la secció 9: es degrada amb desplaçaments grans i no és estable davant d'insercions.
- Paginació per pàgina
És el mateix model amb una altra aritmètica, més còmoda per a qui pinta paginadors:
Avantatge: el client no calcula res. Inconvenient: és exactament igual de fràgil que l'offset, perquè per sota es tradueix a un desplaçament; només canvia la manera de demanar-ho. I hi afegeix una ambigüitat clàssica: la primera pàgina és la 1 o la 0? Qualsevol de les dues val mentre estigui documentada; equivocar-se costa un bug de 20 elements que ningú no veu.
Decisió de la Botiga Aroma: no s'ofereixen pagina/perPagina. Un únic mecanisme (limit/desplacament) és més consistent que dos d'equivalents, i el càlcul de la pàgina el fa el client en una línia.
- Paginació per cursor
En lloc de "salta 5.000", el servidor lliura un punter opac a la posició on es va quedar.
# Primera pàgina: sense cursor
curl "https://api.botigaaroma.example/v1/comandes?limit=20&ordenar=-dataCreacio"HTTP/1.1 200 OK
Link: <https://api.botigaaroma.example/v1/comandes?limit=20&cursor=eyJmIjoiMjAyNi0wMy0xNCIsImkiOiJjb21fNTAwMSJ9>; rel="next"
Content-Type: application/json
{
"dades": [ ],
"cursorSeguent": "eyJmIjoiMjAyNi0wMy0xNCIsImkiOiJjb21fNTAwMSJ9"
}# Pàgina següent: s'envia el cursor rebut
curl "https://api.botigaaroma.example/v1/comandes?limit=20&cursor=eyJmIjoiMjAyNi0wMy0xNCIsImkiOiJjb21fNTAwMSJ9"Com funciona per dins: el cursor codifica (normalment en Base64) els valors de la darrera fila lliurada segons l'ordre vigent —aquí, dataCreacio i id—. La consulta següent no diu "salta 5.000 files", diu "dona'm les files posteriors a (2026-03-14, com_5001)", que l'índex resol a l'instant sigui quina sigui la profunditat.
Regles de la Botiga Aroma per als cursors:
- Són opacs. La documentació prohibeix expressament descodificar-los o construir-los: el seu contingut pot canviar sense previ avís.
- Inclouen l'ordre. Un cursor obtingut amb
?ordenar=-dataCreaciono val per a?ordenar=preuEuros: en barrejar-los,400ambparametre_invalid. - No hi ha
totala les col·leccions paginades per cursor: calcular-lo exigeix comptar tota la taula, que és justament el cost que volíem evitar. Se'n documenta l'absència. - No es pot saltar a la pàgina N: només hi ha "següent" i "anterior". És el preu del model.
- Comparativa i deep paging
| Criteri | Offset (desplacament) |
Per pàgina (pagina) |
Cursor |
|---|---|---|---|
| Facilitat per al client | Alta | Molt alta | Mitjana |
| Saltar a la pàgina N | Sí | Sí | No |
| Total d'elements | Sí | Sí | No (car) |
| Cost a la base de dades | Creix amb la profunditat | Creix amb la profunditat | Constant |
| Estable davant d'insercions | No | No | Sí |
| Escalabilitat | Baixa en col·leccions grans | Baixa | Alta |
| Cacheabilitat | Bona (URLs estables) | Bona | Mitjana |
| Ús típic | Catàlegs, back-office | Webs amb paginador clàssic | Feeds, històric, exportacions |
9.1. El deep paging
Demanar la pàgina 5.000 amb offset obliga el motor a llegir i descartar 100.000 files abans de retornar-ne 20:
-- El que fa realment ?limit=20&desplacament=100000
SELECT * FROM cafes ORDER BY nom ASC, id ASC LIMIT 20 OFFSET 100000;El cost creix linealment amb el desplaçament: la pàgina 1 triga mil·lisegons i la 5.000 pot trigar segons i castigar tota la base de dades. Amb cursor, el cost és el mateix a la pàgina 1 que a la 5.000.
Mitigació de la Botiga Aroma: desplacament màxim de 10.000. Superar-lo retorna 400 amb parametre_invalid i un missatge que remet al mecanisme adequat: "per recórrer la col·lecció completa, fes servir la paginació per cursor".
9.2. Elements duplicats i omesos
L'altre problema de l'offset, il·lustrat amb /comandes ordenat per -dataCreacio:
sequenceDiagram
participant C as Panell intern
participant A as API
C->>A: GET /comandes?limit=3&desplacament=0
A-->>C: [com_5010, com_5009, com_5008]
Note over A: Entra una comanda nova: com_5011<br/>Tot es desplaça una posició
C->>A: GET /comandes?limit=3&desplacament=3
A-->>C: [com_5008, com_5007, com_5006]
Note over C: com_5008 apareix DUES vegades<br/>i cap comanda no s'ha perdut... aquesta vegada
Amb una inserció, es duplica un element; amb un esborrat, se n'omet un, que és pitjor perquè és invisible. En un catàleg que canvia poc, és tolerable; en una exportació comptable, és inacceptable.
Decisió de la Botiga Aroma:
| Col·lecció | Model | Motiu |
|---|---|---|
/cafes |
Offset (limit + desplacament) |
Catàleg petit i estable; el panell necessita saltar a una pàgina i veure el total |
/ressenyes |
Offset | Igual, amb volum moderat |
/clients |
Offset | Igual |
/comandes |
Cursor, amb offset admès fins a desplacament=10000 |
Creix sense límit i rep insercions constants |
/clients/{id}/comandes |
Offset | Són poques per client |
Conviure és legítim sempre que cada col·lecció documenti quin fa servir i els paràmetres no es barregin: enviar cursor i desplacament alhora retorna 400.
- On viatgen les metadades de paginació
Tres llocs possibles, i no són excloents.
a) Al cos, aprofitant l'embolcall decidit a 02-05:
b) En capçaleres pròpies, a l'estil de GitHub:
c) A la capçalera Link (RFC 8288), el mecanisme estàndard del web:
Link: <https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=40>; rel="next",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=0>; rel="first",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=0>; rel="prev",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=120>; rel="last"| Criteri | Cos | Capçaleres X- |
Link (RFC 8288) |
|---|---|---|---|
| Estàndard | No | No (X- desaconsellat per la RFC 6648) |
Sí |
Visible amb HEAD |
No | Sí | Sí |
| Còmode en JavaScript | Molt | Mitjà (cal llegir capçaleres) | Mitjà (cal parsejar) |
| Enllaços ja construïts | No | No | Sí |
| Funciona amb respostes no JSON | No | Sí | Sí |
Decisió de la Botiga Aroma —coherent amb el que es va fixar al mòdul 1—: total al cos i navegació a la capçalera Link.
HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=60>; rel="next",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=20>; rel="prev",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=0>; rel="first",
<https://api.botigaaroma.example/v1/cafes?limit=20&desplacament=120>; rel="last"
{
"dades": [ ],
"total": 137
}Per què aquesta combinació i no una altra:
- És exactament la hipermèdia selectiva de nivell 2 que vam decidir a 01-05: enllaços on aporten (navegació), sense convertir el cos en un document hipermèdia.
Linkés un estàndard ambrelregistrats, no una invenció de la casa, i funciona igual per a JSON, CSV o PDF.- Els enllaços venen construïts: el client no recalcula desplaçaments ni arrossega els filtres a mà. Fixa't que cada enllaç conserva tots els paràmetres de la petició original —filtres, ordre i
camps—, que és justament l'error que més es comet en implementar-ho. totalal cos perquè és una dada del resultat, no de la navegació, i el client JavaScript el té a mà sense parsejar capçaleres.X-Total-Countno es fa servir: seria duplicartotalamb un nom desaconsellat.
Amb cursor, Link porta només next (i prev si el model ho permet), sense first ni last, i el cos no porta total. I a l'última pàgina no hi ha rel="next": la seva absència és el senyal de fi de col·lecció, i així es documenta.
- Cerca de text
Filtrar és igualtat exacta; cercar és una altra cosa: parcial, difusa, amb rellevància. El paràmetre reservat és q:
curl "https://api.botigaaroma.example/v1/cafes?q=yirgacheffe"
curl "https://api.botigaaroma.example/v1/cafes?q=xocolata&torrefaccio=mitja&ordenar=preuEuros"Contracte de q a la Botiga Aroma:
- Cerca a
nom,origeninotesTast, i així queda documentat (una cerca que no diu on cerca és una caixa negra). - Insensible a majúscules i a accents:
q=etiopiatroba "Etiòpia". - Es combina amb els filtres amb I lògic.
- Mínim 2 caràcters; amb menys,
400ambparametre_invalid. - L'ordre per defecte passa a ser per rellevància quan hi ha
q, llevat que s'indiquiordenarexplícitament. I compte: ordenar per rellevància no és estable, així que es desempata peridigual que a la secció 5.
Quan es mereix la cerca un recurs propi?
Quan deixa de ser "filtrar una col·lecció" i es converteix en una funcionalitat amb entitat pròpia:
| Senyal | Exemple | Recurs propi |
|---|---|---|
| Cerca en diversos tipus de recurs alhora | Cafès, articles del blog i ajuda | GET /v1/cerca?q=espresso |
| Retorna metadades de cerca | Puntuació, facetes, suggeriments | Sí |
| Ho resol un altre motor | Elasticsearch, OpenSearch | Sí |
GET /v1/cerca?q=espresso
{
"dades": [
{ "tipus": "cafe", "id": "caf_002", "titol": "Colòmbia Huila", "rellevancia": 0.91,
"_links": { "self": { "href": "/v1/cafes/caf_002" } } },
{ "tipus": "article", "id": "art_12", "titol": "Com preparar un bon espresso", "rellevancia": 0.74 }
],
"total": 2,
"facetes": { "torrefaccio": { "mitja": 1, "fosc": 1 } }
}La Botiga Aroma no crea /cerca a la v1: amb ?q= sobre cada col·lecció n'hi ha prou. Queda anotat com a candidat per quan existeixi el blog.
Consultes complexes per POST
Hi ha un cas legítim en què la consulta no cap a una URL: informes del panell intern amb molts criteris, llistes llargues d'identificadors o expressions que superen el límit pràctic de longitud d'una URL (uns 2.000 caràcters a la majoria de servidors i intermediaris).
POST /v1/comandes/consultes HTTP/1.1
Content-Type: application/json
{
"clientIds": ["cli_842", "cli_843", "…600 més…"],
"estats": ["pagat", "enviat"],
"dataDes": "2026-01-01",
"limit": 100
}És un compromís conscient: es perd la memòria cau i POST deixa de ser "crear" per significar "processa aquesta consulta", i per això el recurs es diu consultes (un substantiu, 02-02) i retorna 200, no 201. La Botiga Aroma no ho inclou a la v1, però deixa escrit el patró per quan el panell ho necessiti, en lloc d'improvisar-lo.
- Valors per defecte, límits i documentació
Aquí es tanca el contracte de col·leccions, i aquesta taula és la que el mòdul 3 implementarà literalment:
| Paràmetre | Per defecte | Màxim | Comportament en excedir-lo |
|---|---|---|---|
limit |
20 | 100 | 400 amb parametre_invalid |
desplacament |
0 | 10.000 | 400 amb parametre_invalid |
camps |
Tots | 30 camps | 400 |
expandir |
Cap | 3 relacions | 400 |
q |
— | 100 caràcters | 400 |
ordenar |
Segons col·lecció | 3 criteris | 400 |
Dues decisions que convé raonar:
limitper sobre del màxim retorna400, no es retalla en silenci. Retallar és temptador ("sigues amable amb el client"), però deixa el consumidor creient que ha rebut 1.000 elements quan només en té 100: paginarà malament i perdrà dades sense assabentar-se'n. Fallar de manera visible és més amable a mitjà termini.- El servidor pagina encara que el client no ho demani.
GET /v1/cafessense paràmetres retorna 20 elements,totali la capçaleraLinkambnext. No hi ha manera de demanar "tot".
I així queda documentat cada paràmetre a la referència (02-08): nom, tipus, obligatorietat, valor per defecte, valors admesos, comportament davant de valors invàlids i un exemple executable. A OpenAPI, això s'escriu una sola vegada com a paràmetres reutilitzables i es referencia des de cada col·lecció:
# Fragment del contracte: paràmetres comuns de col·lecció
components:
parameters:
limit:
name: limit
in: query
description: Nombre màxim d'elements a retornar.
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
example: 20
desplacament:
name: desplacament
in: query
description: Nombre d'elements a ometre des del principi de la col·lecció.
required: false
schema:
type: integer
minimum: 0
maximum: 10000
default: 0
example: 40Errors Comuns i Consells
- No paginar per defecte. L'error més car d'aquesta lliçó: funciona en desenvolupament amb 10 files i tomba producció amb 100.000.
- Ordenar sense desempat únic. Duplicats i elements perduts en paginar, de manera intermitent i irreproduïble.
- Perdre els filtres als enllaços de paginació. El
rel="next"ha d'arrossegarordenar, filtres icamps; si no, la pàgina 2 mostra una cosa diferent de la 1. - Ignorar paràmetres desconeguts en silenci. Una errada (
?torrefacciio=mitja) retorna el catàleg sencer i el client creu que ha filtrat. - Permetre ordenar o filtrar per qualsevol camp. Sense índex, cada consulta és un escaneig complet de la taula.
- Retallar
limitsense avisar. El client en demana 1.000, en rep 100 i creu que la col·lecció en té 100. - Donar
totalen paginació per cursor. Comptar la col·lecció sencera anul·la l'avantatge del cursor. És millor no oferir-lo i documentar-ho. - Barrejar models de paginació a la mateixa col·lecció sense regles:
cursoridesplacamentalhora han de donar400. - Consell: prova sempre amb dades que canvien. Insereix files entre la pàgina 1 i la 2 i comprova què passa. Aquest experiment descobreix la meitat dels bugs de paginació.
- Consell: tria el model segons l'ús, no per moda. El cursor és superior tècnicament, però si el panell necessita "pàgina 7 de 12", l'offset és la resposta correcta.
Exercicis
Exercici 1: construir consultes
Escriu la petició curl completa per a cada necessitat, fent servir només el contracte d'aquesta lliçó:
- Cafès d'Etiòpia o Colòmbia, amb torrefacció clara, entre 12 i 18 euros, del més car al més barat, 10 per pàgina, segona pàgina.
- Comandes del client
cli_842pagades o enviades el març del 2026, les més recents primer. - Ressenyes pendents de moderació amb 3 estrelles o menys, mostrant només
id,puntuacioicomentari. - Cerca de "xocolata" al catàleg, només cafès disponibles, ordenats per preu ascendent.
Exercici 2: diagnosticar una paginació trencada
El panell intern de la Botiga Aroma llista comandes així:
GET /v1/comandes?ordenar=estat&limit=50&desplacament=0
GET /v1/comandes?ordenar=estat&limit=50&desplacament=50
GET /v1/comandes?ordenar=estat&limit=50&desplacament=100Els usuaris es queixen de dues coses: (a) de vegades veuen la mateixa comanda a dues pàgines, i (b) l'última pàgina triga vuit segons amb 400.000 comandes.
Diagnostica cada símptoma i proposa la solució concreta, indicant què s'hi perd.
Exercici 3: dissenyar la paginació d'una col·lecció nova
La Botiga Aroma afegeix /v1/esdeveniments, el registre d'esdeveniments enviats a RàpidEnviaments (evt_9f2c, comanda.pagada, comanda.enviada…). Característiques: creix a raó de milers d'esdeveniments al dia, mai no es modifica ni s'esborra, i RàpidEnviaments el fa servir per reconciliar el que ha rebut, recorrent-lo sencer des del darrer punt conegut.
Dissenya el contracte d'aquesta col·lecció: model de paginació, paràmetres, filtres, ordre per defecte, metadades i capçaleres. Justifica cada decisió.
Solucions
Solució 1
# 1
curl "https://api.botigaaroma.example/v1/cafes?origen=Eti%C3%B2pia,Col%C3%B2mbia&torrefaccio=clar&preuMin=12&preuMax=18&ordenar=-preuEuros&limit=10&desplacament=10"
# 2
curl "https://api.botigaaroma.example/v1/comandes?clientId=cli_842&estat=pagat,enviat&dataDes=2026-03-01&dataFins=2026-03-31&ordenar=-dataCreacio"
# 3
curl "https://api.botigaaroma.example/v1/ressenyes?estat=pendent_moderacio&puntuacioMax=3&camps=id,puntuacio,comentari"
# 4
curl "https://api.botigaaroma.example/v1/cafes?q=xocolata&disponible=true&ordenar=preuEuros"Observacions: a la 1, la segona pàgina amb limit=10 és desplacament=10, i el multivalor d'origen fa servir la coma amb els accents codificats. A la 3 cal un filtre puntuacioMax que no és a la taula de la secció 2: la resposta correcta inclou adonar-se'n i proposar afegir-lo com a canvi retrocompatible, en lloc d'inventar-se ?puntuacio=<=3.
Solució 2
(a) Duplicats: falta el desempat. estat només té tres valors, així que hi ha desenes de milers d'empats i el motor els retorna en un ordre diferent a cada consulta. Solució: el servidor afegeix sempre id com a darrer criteri (ORDER BY estat, id), ho documenta i no depèn que el client ho demani. Cost: cap, llevat d'assegurar l'índex adequat. És una fallada del servidor, no del client.
(b) Lentitud: deep paging. Amb desplacament=399950, el motor llegeix i descarta 399.950 files. Solució: paginació per cursor per a /comandes, més el topall de desplacament=10000. El que s'hi perd: el panell ja no podrà saltar directament a la pàgina 5.000 ni mostrar "pàgina 3 de 8.000", perquè el cursor només ofereix següent i anterior. Mitigació pràctica: gairebé ningú no necessita la pàgina 5.000 —el que necessita és filtrar millor—, així que la solució completa combina cursor i filtres per data i estat perquè el recorregut profund deixi de ser necessari.
Solució 3
Model: paginació per cursor, sense cap dubte. Els tres trets de la col·lecció ho exigeixen: creix sense límit (l'offset es degradaria), és de només escriptura i lectura seqüencial (ningú no necessita "la pàgina 300"), i el cas d'ús és exactament "continua des d'on ho vas deixar", que és la definició d'un cursor.
Contracte proposat:
| Aspecte | Decisió | Justificació |
|---|---|---|
| Paginació | cursor + limit |
Cost constant a qualsevol profunditat |
limit |
Per defecte 50, màxim 200 | Els esdeveniments són petits; convé un lot més gran que l'estàndard per reconciliar ràpid |
| Ordre per defecte | dataCreacio ascendent, desempatat per id |
Es recorre cap endavant en el temps: és l'ordre natural d'un registre |
| Filtres | tipus (comanda.pagada, comanda.enviada), comandaId, dataDes, dataFins, estatLliurament |
Permeten reconciliar per tipus o reintentar els fallits |
total |
No s'ofereix | Comptar milions de files anul·laria l'avantatge del cursor; se'n documenta l'absència |
| Metadades | Link amb rel="next"; sense first/last |
Coherent amb la resta de l'API i amb el model de cursor |
| Fi de col·lecció | Absència de rel="next" |
Senyal documentat; RàpidEnviaments desa el darrer cursor i torna més tard |
| Cos | {"dades": [...], "cursorSeguent": "..."} |
El cursor també al cos, per comoditat del client |
| Immutabilitat | Els esdeveniments no es modifiquen | Un cursor antic continua sent vàlid indefinidament: avantatge decisiu davant de l'offset |
Exemple de resposta:
HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.botigaaroma.example/v1/esdeveniments?limit=50&cursor=eyJmIjoiMjAyNi0wMy0xNFQxMDozMjowMFoiLCJpIjoiZXZ0XzlmMmMifQ>; rel="next"
{
"dades": [
{ "id": "evt_9f2c", "tipus": "comanda.pagada", "comandaId": "com_5001",
"dataCreacio": "2026-03-14T10:32:00Z", "estatLliurament": "lliurat" }
],
"cursorSeguent": "eyJmIjoiMjAyNi0wMy0xNFQxMDozMjowMFoiLCJpIjoiZXZ0XzlmMmMifQ"
}Conclusió
Les col·leccions són on una API s'enfronta a la realitat, i ara la Botiga Aroma té un contracte complet per a elles: filtres explícits per camp, rangs amb Min/Max i Des/Fins, multivalor amb comes, i el rebuig conscient a inventar-se un llenguatge de consulta a la URL; ordenació amb ordenar i -camp, amb la regla crítica del desempat per id que fa que la paginació sigui fiable; tres models de paginació entesos a fons, amb offset per a les col·leccions estables com /cafes i cursor per a les que creixen sense fre com /comandes, més el topall de desplacament que evita el deep paging; les metadades repartides entre total al cos i la capçalera Link estàndard amb els seus rel; cerca amb ?q= i el criteri per saber quan es mereix un recurs propi; i una taula de valors per defecte i màxims que protegeix l'API d'ella mateixa.
Amb això, el contracte de la v1 està pràcticament tancat: recursos, mètodes, codis, representacions i col·leccions. I així que un contracte es publica, comença el problema següent: canviar sense trencar res a ningú. A la lliçó següent, 02-07 Versionat d'APIs, distingirem amb precisió quins canvis són retrocompatibles i quins trencadors, compararem les cinc estratègies de versionat —ruta, query, capçalera, media type i data—, justificarem per què la Botiga Aroma versiona a /v1, i dissenyarem el cicle de depreciació complet amb les capçaleres Deprecation i Sunset.
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
