Tancàvem 06-03 dient que la qualitat d'una API no es mesura el dia del llançament, sinó en la facilitat amb què un integrador continua treballant-hi quan ja no queda ningú de l'equip original a l'empresa. Aquest és el llistó. Durant sis mòduls hem llegit, analitzat i criticat la feina d'altres —inclosa la nostra, a la memòria tècnica de 06-01 i als tres anys de producció de 06-03—. Ara et toca a tu: dissenyaràs i construiràs una API completa des de zero, per fases, amb criteris d'acceptació verificables i una rúbrica amb la qual puguis avaluar-te sense que ningú et corregeixi. Tot el que hem fet fins aquí estava preparant aquest encàrrec.
No és un exercici d'escriure rutes d'Express. És un exercici de decidir: què és un recurs, què garanteix el teu contracte, què passa quan dues persones competeixen per l'última plaça i què li diràs d'aquí a dos anys a qui depengui de tu.
Contingut
- L'encàrrec: l'API d'Aula Aroma
- Dominis alternatius
- Lliurament per fases i criteris d'acceptació
- Rúbrica d'autoavaluació
- Guia d'arrencada: el primer dia
- Pla de treball per setmanes
- Pistes per als punts on gairebé tothom s'encalla
- Solució de referència de la Fase 2
- Errors comuns i consells
- Exercicis
- Conclusió del curs
- L'encàrrec: l'API d'Aula Aroma
La Botiga Aroma vol obrir una línia de negoci nova: Aula Aroma, una plataforma de cursos i tasts presencials de cafè d'especialitat. Tu dissenyes i construeixes la seva API pública des de zero, amb el mateix stack i les mateixes convencions de contracte que hem fet servir durant tot el curs (identificadors amb prefix, camelCase, diners en cèntims per dins i euros per fora, dates ISO-8601 UTC, col·leccions embolcallades en {"dades": [...], "total": n}, errors amb catàleg de codis, versionat a la ruta).
1.1 El domini
| Concepte | Descripció |
|---|---|
| Curs | Producte formatiu repetible: "Tast sensorial d'orígens africans", "Latte art nivell 1". Té títol, descripció, durada, nivell i preu base. |
| Sessió | Celebració concreta d'un curs: data i hora, seu, aforament, instructor assignat. Un curs té moltes sessions. |
| Inscripció | Una persona ocupa una plaça d'una sessió. Té estat, preu pagat i data. |
| Llista d'espera | Persones que es van voler inscriure amb l'aforament ple i esperen una vacant. |
| Assistent | Persona identificada (compte amb correu i contrasenya) que s'inscriu. Pot ser client de la botiga o no. |
| Valoració | Puntuació d'1 a 5 i comentari que un assistent deixa sobre una sessió a la qual va assistir. |
| Instructor | Qui imparteix sessions. Té biografia, especialitats i una valoració mitjana derivada. |
1.2 Regles de negoci
Aquestes regles no són decoració: són la raó per la qual el projecte és interessant. Cadascuna obliga a una decisió de disseny.
- Aforament. Cada sessió té un nombre màxim de places. No hi pot haver mai més inscripcions confirmades que places, ni tan sols amb peticions simultànies.
- Tancament d'inscripcions. No s'admeten inscripcions noves a partir de
Xhores abans del començament (parametritzable per curs; per defecte 24). - Llista d'espera. Si l'aforament és ple, la persona pot entrar a la llista d'espera. Quan algú cancel·la, la primera de la llista passa a tenir una finestra de temps per confirmar.
- Cancel·lació i reemborsament. Cancel·lar amb més de 72 hores d'antelació reemborsa el 100 %; entre 72 i 24 hores, el 50 %; amb menys de 24 hores, res. La cancel·lació per part d'Aula Aroma reemborsa sempre el 100 %.
- Valoració condicionada. Un assistent només pot valorar una sessió si la seva inscripció figura com a assistida, i només una vegada.
- Descomptes. Els clients de la Botiga Aroma amb compres els últims 12 mesos tenen un 15 % de descompte; hi ha codis promocionals amb import fix o percentatge; els descomptes no s'acumulen tret d'indicació explícita.
1.3 Els quatre reptes de debò
- Places limitades i concurrència. Dues peticions alhora per a l'última plaça. La comprovació "queda lloc?" i la reserva han de ser atòmiques. Ja vas veure el patró a
06-01: la condició viatja dins de l'UPDATE, no en unSELECTprevi. - Cancel·lacions i llistes d'espera. Cancel·lar dispara un efecte en cadena (alliberar plaça, promocionar de la llista, notificar, calcular el reemborsament). Això és un
DELETE? És un recurs? - Dates i fusos horaris. Les sessions són presencials i passen en una hora local concreta. El contracte diu UTC, però "les sessions del dissabte" depèn del fus de qui pregunta. Els canvis d'horari d'estiu existeixen i et mossegaran.
- Preus i descomptes. Cèntims per dins, euros per fora (
02-05). El preu final depèn de qui pregunta, quan i amb quin codi. Es calcula a la representació del curs o només en inscriure's?
- Dominis alternatius
Si Aula Aroma no et motiva, tria'n un altre. El llistó d'exigència és el mateix: ha de tenir com a mínim un recurs amb restricció de concurrència, un flux de canvi d'estat i una relació no trivial.
| Domini | Quin repte afegeix |
|---|---|
| Gestió de biblioteca | Exemplars davant d'obres: el mateix llibre té diverses còpies físiques, i el préstec es fa sobre una còpia, no sobre el títol. Modelar aquesta diferència sense filtrar-la al consumidor és l'exercici. |
| Helpdesk d'incidències | La màquina d'estats és el cor del domini (oberta, assignada, en espera del client, resolta, tancada) amb transicions no lliures, i l'autorització depèn del rol i de la pertinença. |
| Reserva de sales | Solapament temporal: la restricció no és un comptador, és un interval que no es pot creuar amb un altre, i les reserves periòdiques multipliquen el problema. |
- Lliurament per fases i criteris d'acceptació
Una fase per mòdul. No passis a la següent sense complir els criteris: la meitat del valor del projecte és a resistir la temptació d'escriure codi a la fase 1.
Fase 1 — Anàlisi (mòdul 1)
Lliurable: docs/analisi.md.
| # | Criteri d'acceptació |
|---|---|
| 1.1 | Llista de consumidors identificats (web pública, tauler d'administració, app d'instructors, integració amb la botiga) amb el que necessita cadascun. |
| 1.2 | Com a mínim 12 casos d'ús redactats com a "com a rol, vull acció per a finalitat". |
| 1.3 | Requisits no funcionals quantificats: latència objectiu, volum esperat, disponibilitat, retenció de dades. |
| 1.4 | Glossari del domini amb 15+ termes i el seu nom exacte al contracte (en català, coherent). |
| 1.5 | Justificació de per què REST i no una altra opció, amb referència a 01-07. |
| 1.6 | Nivell objectiu al model de Richardson (01-05) declarat i raonat. |
Fase 2 — Contracte (mòdul 2)
Lliurable: openapi.yaml + docs/contracte.md.
| # | Criteri d'acceptació |
|---|---|
| 2.1 | Mapa de recursos i URIs complet, amb substantius en plural i sense verbs (02-02). |
| 2.2 | Taula mètode × recurs amb el codi d'estat d'èxit i els d'error de cada combinació (02-03, 02-04). |
| 2.3 | Esquemes de representació de cada recurs, amb tipus, obligatorietat i exemple. |
| 2.4 | Catàleg d'errors propi, amb codi snake_case, estat HTTP i missatge. |
| 2.5 | Paginació i filtres definits per col·lecció, indicant quina fa servir limit/desplacament i quina cursor (02-06). |
| 2.6 | Estratègia de versionat escrita, amb què compta com a canvi compatible (02-07). |
| 2.7 | openapi.yaml en OpenAPI 3.1 que passa spectral lint sense errors (05-02). |
| 2.8 | Com a mínim un exemple de petició i resposta per operació. |
Fase 3 — Implementació (mòdul 3)
Lliurable: codi a src/, migracions/, proves/.
| # | Criteri d'acceptació |
|---|---|
| 3.1 | Estructura per capes respectada: les rutes no toquen la base de dades i els repositoris no coneixen HTTP (03-05). |
| 3.2 | Validació amb Zod de cos, paràmetres de ruta i de consulta, amb errors traduïts al format del catàleg (03-04). |
| 3.3 | Migracions versionades i reproduïbles des de zero amb una sola ordre. |
| 3.4 | La reserva de plaça es fa dins d'una transacció i és correcta sota concurrència (amb prova que ho demostri). |
| 3.5 | Autenticació JWT + bcrypt i autorització per rol i pertinença (03-06). |
| 3.6 | Gestió d'errors unificada: cap 500 sense registrar i cap traça filtrada al client (03-07). |
| 3.7 | Proves amb node:test + Supertest, cobertura ≥ 70 % als serveis, amb casos d'èxit i d'error (03-08). |
Fase 4 — Enduriment (mòdul 4)
Lliurable: middleware, configuració i docs/seguretat.md.
| # | Criteri d'acceptació |
|---|---|
| 4.1 | helmet actiu i capçaleres revisades una a una, no per defecte cec (04-02). |
| 4.2 | Rate limiting amb límits diferents per a escriptura i lectura, i 429 amb Retry-After (04-04). |
| 4.3 | CORS amb llista blanca explícita d'orígens, no * en producció (04-05). |
| 4.4 | ETag + If-None-Match en almenys dues col·leccions de lectura freqüent, amb 304 verificat en una prova (04-06). |
| 4.5 | Logs estructurats amb pino, identificador de correlació per petició i sense dades personals en clar (04-07). |
| 4.6 | Mètriques amb prom-client: comptador de peticions, histograma de latència i almenys una mètrica de negoci (places ocupades, per exemple). |
Fase 5 — Eines (mòdul 5)
Lliurable: postman/, Dockerfile, .github/workflows/ci.yml.
| # | Criteri d'acceptació |
|---|---|
| 5.1 | Col·lecció de Postman que recorre el flux complet (registre → inscripció → cancel·lació → valoració) i passa amb newman run (05-01). |
| 5.2 | Swagger UI servit des de la mateixa API a /v1/docs, alimentat per openapi.yaml. |
| 5.3 | Validació de contracte a les proves: com a mínim una prova comprova que la resposta real encaixa amb l'esquema declarat (05-04). |
| 5.4 | Dockerfile multietapa que arrenca l'API amb un sol docker run. |
| 5.5 | CI a GitHub Actions que executa lint, Spectral, proves i Newman, i falla si alguna cosa falla (05-05). |
Fase 6 — Memòria (mòdul 6)
Lliurable: docs/memoria.md + docs/decisions/*.md (ADR).
| # | Criteri d'acceptació |
|---|---|
| 6.1 | Com a mínim 6 ADR amb decisió, context, alternatives descartades i conseqüències, a l'estil de 06-01. |
| 6.2 | Autocrítica honesta: tres coses que refaries i per què. |
| 6.3 | Pla d'evolució a un any, amb què afegiries sense trencar i què obligaria a una v2 (06-03). |
| 6.4 | Política de deprecació escrita: capçaleres, terminis i comunicació. |
| 6.5 | Comparació breu amb el cas de CafeSocial de 06-02: quines decisions teves no serien vàlides en un altre domini. |
- Rúbrica d'autoavaluació
Puntua cada criteri amb 0 (insuficient), 0,6 (correcte) o 1 (excel·lent) i multiplica pel pes. Per sota de 60 punts, torna a la fase més fluixa abans de continuar.
| Criteri | Pes | Insuficient | Correcte | Excel·lent |
|---|---|---|---|---|
| Disseny del contracte | 20 | URIs amb verbs, codis inventats, errors incoherents | Recursos i mètodes correctes, errors catalogats | Contracte que s'entén sense llegir el codi; casos límit previstos |
| Fidelitat al domini | 15 | Falten regles de negoci o es contradiuen | Totes les regles implementades | Regles implementades i expressades al contracte, no amagades |
| Correcció sota concurrència | 15 | Es pot sobrevendre l'aforament | Reserva atòmica dins d'una transacció | A més, provada amb peticions simultànies reals |
| Qualitat de la implementació | 10 | Tot a les rutes, sense capes | Capes respectades, validació completa | Codi llegible, serveis reutilitzables, sense repetició |
| Seguretat | 10 | Sense autenticació o amb secrets al repositori | JWT, rols, helmet, CORS, rate limiting | Autorització per pertinença i superfície mínima exposada |
| Proves | 10 | Anecdòtiques o inexistents | ≥ 70 % als serveis, casos d'error | Unitàries + integració + e2e + contracte |
| Documentació | 10 | README escarransit | OpenAPI vàlid i Swagger UI | Exemples executables i guia de primeres passes per a l'integrador |
| Observabilitat | 5 | console.log |
Logs estructurats i mètriques bàsiques | Correlació de peticions i mètriques de negoci útils |
| Memòria i autocrítica | 5 | Descripció del que s'ha fet | Decisions justificades | Alternatives descartades i pla d'evolució creïble |
| Total | 100 |
- Guia d'arrencada: el primer dia
# 1. Projecte i dependències
mkdir aula-aroma && cd aula-aroma
npm init -y
npm pkg set type="module"
npm pkg set engines.node=">=20"
npm install express@4 zod better-sqlite3 jsonwebtoken bcrypt \
helmet cors express-rate-limit pino pino-http prom-client \
swagger-ui-express yaml
npm install --save-dev supertest @stoplight/spectral-cli newman c8
# 2. Estructura per capes (la mateixa de tot el curs)
mkdir -p src/{config,rutes,controladors,serveis,repositoris,esquemes,middleware,errors,observabilitat}
mkdir -p migracions proves/{unitaries,integracio,e2e,ajudes} docs/decisions postman
touch src/app.js src/servidor.js openapi.yaml docs/analisi.md
# 3. Scripts mínims
npm pkg set scripts.dev="node --watch src/servidor.js"
npm pkg set scripts.migrar="node migracions/executar.js"
npm pkg set scripts.prova="node --test proves/"
npm pkg set scripts.contracte="spectral lint openapi.yaml"
# 4. Higiene des del minut u
printf "node_modules\n*.db\n.env\n" > .gitignore
git init && git add -A && git commit -m "Estructura inicial d'Aula Aroma"Abans d'escriure la primera ruta, escriu el primer ADR. docs/decisions/0001-versionat-a-la-ruta.md et costarà deu minuts i t'estalviarà una discussió amb tu mateix d'aquí a tres setmanes.
- Pla de treball per setmanes
Estimat per a unes 8-10 hores setmanals. Ajusta el calendari, no l'ordre.
| Setmana | Focus | En acabar hauries de tenir |
|---|---|---|
| 1 | Fase 1 completa | Anàlisi, glossari i casos d'ús tancats. Zero codi. |
| 2 | Fase 2: recursos, mètodes, errors | Mapa d'URIs i catàleg d'errors revisats dues vegades |
| 3 | Fase 2: openapi.yaml |
Contracte que passa Spectral i exemples de cada operació |
| 4 | Fase 3: esquelet, migracions, CRUD de cursos i sessions | GET/POST funcionant amb validació i errors unificats |
| 5 | Fase 3: inscripcions, aforament, cancel·lació, llista d'espera | La lògica difícil resolta i provada sota concurrència |
| 6 | Fase 3: autenticació, autorització, cobertura | Proves verdes i ≥ 70 % als serveis |
| 7 | Fase 4 completa | Capçaleres, límits, CORS, ETag, logs i mètriques |
| 8 | Fase 5 completa | Postman + Newman, Swagger UI, Docker i CI en verd |
| 9 | Fase 6 i repàs amb la rúbrica | Memòria, ADR i puntuació honesta |
- Pistes per als punts on gairebé tothom s'encalla
7.1 La inscripció és un recurs propi o un subrecurs de la sessió?
Totes dues coses, i no és cap parany. Es crea on viu la restricció (POST /v1/sessions/{id}/inscripcions: la plaça pertany a la sessió) i es consulta i es manipula per la seva identitat pròpia (GET /v1/inscripcions/{id}), perquè una inscripció té cicle de vida, apareix a "les meves inscripcions" i es referencia des de valoracions i factures. La regla pràctica: si alguna cosa es llista de manera independent del seu pare o s'enllaça des d'altres llocs, necessita URI canònica pròpia (02-02).
7.2 Places i concurrència: la condició va dins de l'UPDATE
L'error clàssic és llegir, comprovar en JavaScript i escriure. Entre la lectura i l'escriptura hi cap una altra petició sencera. La comprovació ha de ser part de l'escriptura, com a 06-01:
-- Correcte: la condició viu a l'UPDATE. Si retorna 0 files, no hi havia plaça.
UPDATE sessions
SET places_ocupades = places_ocupades + 1
WHERE id = ?
AND places_ocupades < aforament
AND estat = 'oberta';// serveis/inscripcions.js
export function inscriure(sessioId, assistentId) {
return db.transaction(() => {
const res = repoSessions.ocuparPlaca(sessioId); // l'UPDATE de dalt
if (res.changes === 0) {
// No sabem si és aforament ple o sessió tancada: ho preguntem ara, ja sense cursa
const sessio = repoSessions.buscarPerId(sessioId);
if (!sessio) throw new ErrorNoTrobat('sessio_no_trobada');
if (sessio.estat !== 'oberta') throw new ErrorConflicte('inscripcions_tancades');
throw new ErrorConflicte('aforament_complet'); // 409, amb enllaç a la llista d'espera
}
return repoInscripcions.crear({ sessioId, assistentId, estat: 'confirmada' });
})();
}I afegeix la xarxa de seguretat a l'esquema, perquè la base de dades no depengui que el teu codi sigui perfecte:
CREATE TABLE inscripcions (
id TEXT PRIMARY KEY,
sessio_id TEXT NOT NULL REFERENCES sessions(id),
assistent_id TEXT NOT NULL REFERENCES assistents(id),
estat TEXT NOT NULL CHECK (estat IN ('confirmada','cancellada','assistida','absent')),
preu_cent INTEGER NOT NULL CHECK (preu_cent >= 0),
creada_el TEXT NOT NULL
);
-- Una persona no pot tenir dues inscripcions vives a la mateixa sessió
CREATE UNIQUE INDEX ux_inscripcio_viva
ON inscripcions (sessio_id, assistent_id)
WHERE estat <> 'cancellada';7.3 400 o 409?
La pregunta correcta és: reenviar la mateixa petició més tard podria funcionar?
| Situació | Codi | Per què |
|---|---|---|
puntuacio: 9 en una valoració d'1 a 5 |
400 |
La petició està mal formada; no serà vàlida mai |
Falta sessioId |
400 |
Sintaxi del cos |
| Aforament complet | 409 |
La petició és vàlida; l'estat del servidor ho impedeix, i pot canviar |
| Inscripcions ja tancades per l'antelació | 409 |
Vàlida, però incompatible amb l'estat actual |
| Valorar una sessió a la qual no vas assistir | 403 |
És una qüestió de permís sobre el recurs, no de forma |
| Sessió inexistent | 404 |
No hi ha recurs al qual aplicar l'operació |
Un truc extra: 422 és legítim quan el cos és sintàcticament vàlid però semànticament impossible (data de fi anterior a la d'inici). Tria una de les dues convencions (400 per a tot o 400/422 separats) i aplica-la sense excepcions; el que trenca els integradors és la incoherència, no l'elecció (02-04).
7.4 Dates i fusos horaris
Desa sempre en UTC i emet ISO-8601 amb Z. Però afegeix a la sessió el fus de la seu ("fusHorari": "Europe/Madrid") i l'hora local ja formatada si els teus clients l'han de pintar: no obliguis cada consumidor a resoldre-ho. Per filtrar, accepta un rang explícit i no un concepte ambigu:
Evita ?data=2027-03-28, perquè "aquell dia" depèn del fus de qui pregunta i el 28 de març té 23 hores a Madrid. Si tot i així el vols oferir per comoditat, documenta explícitament que s'interpreta a la zona de la seu.
7.5 La llista d'espera és un estat, no una altra col·lecció
És temptador crear una taula i un recurs paral·lels. Però una persona a la llista d'espera és una inscripció amb estat: "en_espera" i una posicio. Així, promocionar algú és un canvi d'estat i no un trasllat entre col·leccions, "les meves inscripcions" ho retorna tot amb un filtre i no dupliques les regles d'unicitat. Exposa /v1/sessions/{id}/llista-espera com a vista filtrada de les inscripcions d'aquella sessió, no com un magatzem diferent.
7.6 No acoblis el JSON a les taules
La teva taula té places_ocupades, aforament i preu_cent. La teva representació hauria d'oferir el que el consumidor necessita: placesDisponibles (derivat), preu en euros amb dos decimals, estat calculat ("oberta", "completa", "tancada"). Si més endavant canvies el càlcul de places, el contracte no se n'assabenta. Aquesta és la línia que separa una API d'un formulari sobre una base de dades (02-01, 03-05).
- Solució de referència de la Fase 2
Et dono resolt el contracte perquè tinguis una vara de mesurar. No el copiïs sense entendre'l: cada fila té un perquè, i hi ha decisions discutibles a propòsit. La resta de fases és cosa teva.
8.1 Mapa d'URIs
| Mètode i URI | Què fa | Èxit | Errors freqüents | Auth |
|---|---|---|---|---|
GET /v1/cursos |
Llista cursos, amb nivell, duracioMax, q, limit/desplacament |
200 |
400 |
No |
GET /v1/cursos/{id} |
Detall amb _links a les seves sessions |
200 |
404 |
No |
POST /v1/cursos |
Crea un curs | 201 + Location |
400, 403 |
Admin |
PATCH /v1/cursos/{id} |
Modifica camps solts | 200 |
400, 404, 409 |
Admin |
GET /v1/cursos/{id}/sessions |
Sessions d'un curs, filtrables per des/fins/seu |
200 |
400, 404 |
No |
GET /v1/sessions |
Totes les sessions, mateix filtratge | 200 |
400 |
No |
GET /v1/sessions/{id} |
Detall amb placesDisponibles i estat |
200 |
404 |
No |
POST /v1/sessions |
Programa una sessió d'un curs | 201 |
400, 404, 409 |
Admin |
POST /v1/sessions/{id}/inscripcions |
Ocupa plaça (accepta Idempotency-Key) |
201 |
400, 401, 404, 409 |
Assistent |
GET /v1/sessions/{id}/inscripcions |
Inscrits de la sessió | 200 |
403, 404 |
Instructor/Admin |
GET /v1/sessions/{id}/llista-espera |
Vista d'inscripcions en_espera, ordenades per posició |
200 |
404 |
Instructor/Admin |
POST /v1/sessions/{id}/llista-espera |
Entra a la llista d'espera quan hi ha aforament complet | 201 |
401, 404, 409 |
Assistent |
GET /v1/inscripcions |
Les meves inscripcions, filtre per estat |
200 |
401 |
Assistent |
GET /v1/inscripcions/{id} |
Detall canònic | 200 |
401, 403, 404 |
Propietari/Admin |
PUT /v1/inscripcions/{id}/cancellacio |
Cancel·la i retorna el reemborsament calculat | 200 |
401, 403, 404, 409 |
Propietari/Admin |
POST /v1/valoracions |
Valora una sessió assistida | 201 |
400, 401, 403, 409 |
Assistent |
GET /v1/valoracions |
Llista per sessioId, cursId o instructorId, amb cursor |
200 |
400 |
No |
DELETE /v1/valoracions/{id} |
Retira la pròpia valoració | 204 |
401, 403, 404 |
Propietari/Admin |
GET /v1/instructors |
Llista amb especialitats | 200 |
400 |
No |
GET /v1/instructors/{id} |
Detall amb valoracioMitjana i totalSessions |
200 |
404 |
No |
Dues decisions que mereixen comentari. La cancel·lació és un subrecurs amb PUT, no un DELETE /inscripcions/{id}: cancel·lar no esborra res (la inscripció continua existint, amb historial i reemborsament), té resultat propi per retornar i és idempotent —cancel·lar dues vegades deixa el mateix estat—. I POST /v1/valoracions no penja de la sessió perquè la valoració es llista i es consulta de manera transversal (per instructor, per curs, per autor) i l'enllaç natural és la inscripció, que ja identifica sessió i persona.
8.2 Catàleg d'errors
| Codi | HTTP | Quan |
|---|---|---|
dades_invalides |
400 | Fallada de validació; detalls porta camp i motiu |
rang_dates_invalid |
400 | des posterior a fins, o format no ISO-8601 |
cursor_invalid |
400 | Cursor mal format o caducat |
credencials_invalides |
401 | Usuari o contrasenya incorrectes |
token_caducat |
401 | JWT caducat; el client l'ha de renovar |
permis_insuficient |
403 | Rol sense accés a l'operació |
no_vas_assistir_a_la_sessio |
403 | Intent de valorar sense inscripció assistida |
curs_no_trobat |
404 | Identificador inexistent |
sessio_no_trobada |
404 | Identificador inexistent |
inscripcio_no_trobada |
404 | Identificador inexistent o d'una altra persona sense privilegis |
aforament_complet |
409 | No queden places; _links.llistaEspera indica la sortida |
inscripcions_tancades |
409 | S'ha superat l'antelació mínima |
ja_inscrit |
409 | Ja existeix una inscripció viva en aquella sessió |
ja_valorada |
409 | Només s'admet una valoració per inscripció |
sessio_ja_cancellada |
409 | Operació sobre una sessió anul·lada |
codi_promocional_no_aplicable |
409 | Caducat, exhaurit o incompatible |
massa_peticions |
429 | Límit superat; vegeu Retry-After |
error_intern |
500 | Fallada no prevista; es registra amb identificador de correlació |
Exemple de cos d'error, en el format del curs:
{
"error": {
"codi": "aforament_complet",
"missatge": "La sessió ses_0412 no té places disponibles.",
"detalls": [
{ "camp": "sessioId", "motiu": "aforament 12 de 12 ocupat" }
]
},
"_links": {
"llistaEspera": "/v1/sessions/ses_0412/llista-espera",
"sessionsAlternatives": "/v1/cursos/cur_007/sessions?des=2027-04-01T00:00:00Z"
}
}Fixa't en el detall: l'error no només diu que no, diu què fer a continuació. Això és HATEOAS aplicat amb criteri (01-05), sense cerimònia inútil.
8.3 Fragment d'openapi.yaml
openapi: 3.1.0
info:
title: API d'Aula Aroma
version: 1.0.0
description: Cursos, sessions presencials i inscripcions de la Botiga Aroma.
servers:
- url: https://api.aula-aroma.example/v1
paths:
/sessions/{sessioId}/inscripcions:
post:
summary: Inscriu l'assistent autenticat en una sessió
operationId: crearInscripcio
tags: [Inscripcions]
security: [{ bearerAuth: [] }]
parameters:
- name: sessioId
in: path
required: true
schema: { type: string, pattern: '^ses_[0-9]{4}$' }
- name: Idempotency-Key
in: header
required: false
description: Repetir la petició amb la mateixa clau no crea una segona inscripció.
schema: { type: string, maxLength: 64 }
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
codiPromocional: { type: string, maxLength: 24 }
additionalProperties: false
responses:
'201':
description: Inscripció confirmada
headers:
Location:
schema: { type: string }
example: /v1/inscripcions/ins_10233
content:
application/json:
schema: { $ref: '#/components/schemas/Inscripcio' }
'409':
description: Aforament complet o inscripcions tancades
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
schemas:
Inscripcio:
type: object
required: [id, sessioId, assistentId, estat, preu, creadaEl]
properties:
id: { type: string, example: ins_10233 }
sessioId: { type: string, example: ses_0412 }
assistentId: { type: string, example: ast_0091 }
estat:
type: string
enum: [confirmada, en_espera, cancellada, assistida, absent]
posicioEspera:
type: [integer, 'null']
description: Posició a la llista d'espera; null si està confirmada.
preu: { type: string, example: '45.00', description: Euros amb dos decimals }
descompteAplicat: { type: string, example: '15%' }
creadaEl: { type: string, format: date-time }
_links:
type: object
properties:
self: { type: string }
sessio: { type: string }
cancellacio: { type: string }
Error:
type: object
required: [error]
properties:
error:
type: object
required: [codi, missatge]
properties:
codi: { type: string, example: aforament_complet }
missatge: { type: string }
detalls:
type: array
items:
type: object
properties:
camp: { type: string }
motiu: { type: string }8.4 El flux que has de fer funcionar
sequenceDiagram
participant C as Client
participant A as API Aula Aroma
participant D as SQLite
C->>A: POST /v1/sessions/ses_0412/inscripcions
A->>A: Valida JWT i cos (Zod)
A->>D: BEGIN + UPDATE sessions ... WHERE ocupades < aforament
alt Quedava placa
D-->>A: 1 fila modificada
A->>D: INSERT inscripcio (confirmada) + COMMIT
A-->>C: 201 Created + Location
else Aforament complet
D-->>A: 0 files modificades
A->>D: ROLLBACK
A-->>C: 409 aforament_complet + _links.llistaEspera
end
Errors Comuns i Consells
- Començar pel codi. El símptoma és un
openapi.yamlescrit al final per documentar el que ja existeix. El contracte es dissenya abans; si no, acabes exposant el teu esquema de taules i descobrint a la fase 5 que la paginació no encaixa. - Comprovar l'aforament amb un
SELECTprevi. Funciona en desenvolupament, on mai no hi ha dues peticions alhora, i sobrevèn el primer dia real. Escriu una prova que llanci 20 inscripcions simultànies contra una sessió de 5 places i exigeixi exactament 5 confirmades. - Fer servir
DELETEper cancel·lar. Perds historial, no pots retornar el reemborsament calculat i et quedes sense lloc on posar el motiu. Modela la cancel·lació com una transició d'estat. - Duplicar la llista d'espera en una altra taula i un altre recurs. Multiplica regles, convida a incoherències (algú confirmat i en espera) i complica "les meves inscripcions".
- Retornar cèntims en uns llocs i euros en d'altres. Tria, documenta-ho al contracte i posa-ho al convertidor de la capa de representació, no a cada controlador (
02-05). - Missatges d'error com a única informació. Els clients programen contra el
codi, no contra el text. Si canvies un missatge no passa res; si canvies un codi, trenques integracions (06-03). - Autoritzar només per rol. "És assistent" no n'hi ha prou: cal comprovar que aquella inscripció és seva. La pertinença es verifica al servei, amb l'identificador del token, mai amb un identificador que vingui del cos.
- Deixar l'observabilitat per al final. Afegir l'identificador de correlació quan ja hi ha 40 fitxers costa cinc vegades més que posar-lo el primer dia.
- Consell de ritme: si una fase se t'encalla més de dos dies, lliura la versió mínima que compleixi els criteris i continua. Tornar-hi amb el projecte sencer muntat és més fàcil que perfeccionar en el buit.
- Consell final: desa cada decisió dubtosa en un ADR en el moment de dubtar. La memòria de la fase 6 s'escriurà gairebé sola.
Exercicis
Exercici 1 — Classificar decisions de modelatge
Per a cada element d'Aula Aroma, decideix si ha de ser (a) un recurs amb URI pròpia, (b) un subrecurs, (c) un camp d'un altre recurs, o (d) un paràmetre de consulta. Justifica-ho en una frase.
- La llista d'espera d'una sessió.
- El descompte del 15 % per als clients de la botiga.
- La valoració mitjana d'un instructor.
- La cancel·lació d'una inscripció.
- Les sessions d'un curs en un rang de dates.
Exercici 2 — Triar el codi d'estat
Indica el codi HTTP i el codi d'error del catàleg per a cada situació:
POST /v1/valoracionsamb"puntuacio": 0.POST /v1/sessions/ses_0412/inscripcionsen una sessió amb 12 de 12 places.PUT /v1/inscripcions/ins_555/cancellacioquanins_555pertany a una altra persona.POST /v1/sessions/ses_9999/inscripcionssense que existeixises_9999.POST /v1/sessions/ses_0412/inscripcions6 hores abans d'una sessió amb antelació mínima de 24 hores.
Exercici 3 — Esquema Zod de la inscripció
Escriu l'esquema Zod que valida el cos de POST /v1/sessions/{sessioId}/inscripcions i el de PUT /v1/inscripcions/{id}/cancellacio, sabent que a la cancel·lació s'admet un motiu opcional de fins a 200 caràcters i que cap camp extra no s'hi pot colar.
Solucions
Exercici 1
- (b) Subrecurs de la sessió, però com a vista filtrada de les seves inscripcions:
GET /v1/sessions/{id}/llista-espera. No és una col·lecció independent; per dins ésestat: "en_espera". - (c) Camp derivat del preu a la representació (
preuBase,preu,descompteAplicat). No té identitat pròpia ni es consulta per si mateix; es calcula en representar i en inscriure. - (c) Camp calculat de l'instructor (
valoracioMitjana,totalValoracions). El consumidor la vol al costat de l'instructor, i obligar-lo a una segona petició per un número és mal disseny. - (b) Subrecurs de la inscripció amb
PUT: té efectes propis (reemborsament), resultat per retornar i és idempotent. UnDELETEperdria la informació. - (d) Paràmetres de consulta
desifinssobreGET /v1/cursos/{id}/sessions. Un filtre no crea un recurs nou (02-06).
Exercici 2
| # | Codi | Error | Motiu |
|---|---|---|---|
| 1 | 400 |
dades_invalides |
Fora del rang 1-5: la petició no serà mai vàlida |
| 2 | 409 |
aforament_complet |
Petició vàlida, estat del servidor incompatible i canviant |
| 3 | 403 |
permis_insuficient |
Recurs existent sobre el qual no es té permís |
| 4 | 404 |
sessio_no_trobada |
El recurs destí no existeix |
| 5 | 409 |
inscripcions_tancades |
Vàlida, però la finestra temporal ja s'ha tancat |
Al cas 3, si prefereixes no revelar l'existència d'inscripcions alienes, un 404 és defensable: és una decisió de seguretat, no de semàntica, i s'ha de documentar (04-02).
Exercici 3
// esquemes/inscripcions.js
import { z } from 'zod';
// Cos de POST /v1/sessions/{sessioId}/inscripcions
export const esquemaCrearInscripcio = z.object({
codiPromocional: z.string().trim().min(3).max(24).regex(/^[A-Z0-9-]+$/, {
message: 'Només majúscules, dígits i guions'
}).optional()
}).strict(); // strict() rebutja camps extra: res de colar "estat" o "preu"
// Paràmetre de ruta, validat a part per no barrejar responsabilitats
export const esquemaSessioId = z.object({
sessioId: z.string().regex(/^ses_[0-9]{4}$/, { message: 'Identificador de sessió no vàlid' })
});
// Cos de PUT /v1/inscripcions/{id}/cancellacio
export const esquemaCancellacio = z.object({
motiu: z.string().trim().max(200).optional()
}).strict();El detall important: el client no envia assistentId. Surt del JWT. Si l'acceptessis del cos, qualsevol podria inscriure una altra persona; és la fallada d'autorització més comuna en projectes d'aquest tipus (03-06).
Conclusió
Hem recorregut un camí llarg. Vam començar al mòdul 1 preguntant-nos què és realment una API i per què HTTP, nascut per servir documents, va acabar sent la millor base per connectar sistemes; vam passar per Richardson, per HATEOAS i per la comparació honesta amb SOAP, GraphQL i gRPC. Al mòdul 2 vam deixar de programar per dissenyar: recursos, URIs, mètodes, codis, representacions, paginació, versionat i documentació. El mòdul 3 va convertir aquell contracte en codi amb capes, validació, persistència, autenticació, errors i proves. El mòdul 4 el va endurir per al món real: seguretat, OAuth, límits, CORS, memòria cau i observabilitat. El mòdul 5 ens va donar les eines que fan sostenible la feina diària. I el mòdul 6 ens ha ensenyat, amb tres casos, que una API es jutja per com envelleix.
Si d'aquí a uns anys oblides els detalls —i els oblidaràs, perquè les versions d'Express canvien i les biblioteques se substitueixen—, queda't amb el que no caduca:
- El contracte és el producte. El codi és reemplaçable; la promesa que has fet a qui et consumeix, no.
- Es dissenya per al consumidor, no per a la base de dades. L'estructura de les teves taules és un assumpte teu. Que es filtri al JSON és l'origen de la meitat de les males APIs.
- HTTP resol més del que sembla. Codis d'estat, capçaleres condicionals, memòria cau, negociació de contingut, idempotència: gairebé sempre que vulguis inventar un mecanisme, comprova abans si ja existeix.
- La compatibilitat és un compromís. Afegir sense trencar no és una limitació tècnica, és una forma de respecte cap a gent que va confiar en tu.
- La seguretat i l'observabilitat no són opcionals. Una API sense autorització correcta és una bretxa esperant data; una API sense traces és una caixa negra el dia de l'incident.
- No hi ha un disseny REST universal. Ho vam veure a
06-02: el que era obvi per a una botiga deixava de ser-ho per a una xarxa social. Les regles són eines de pensament, no dogmes.
Com continuar? Llegeix les fonts de primera mà: les RFC d'HTTP (9110 a 9114), la de Problem Details (9457), l'especificació d'OpenAPI, la d'OAuth 2.1 i OIDC. Estudia APIs públiques que es prenen seriosament el seu contracte —Stripe és una classe magistral de versionat i errors; GitHub, de paginació, condicionalitat i evolució al llarg de més d'una dècada—. Agafa qualsevol API amb la qual treballis i critica-la amb el que ara saps: mira'n els codis d'estat, la paginació, les capçaleres de memòria cau, la política de deprecació. I si pots, contribueix-hi: revisa contractes aliens, obre incidències a les especificacions del teu equip, escriu la documentació que a tu t'hauria agradat trobar.
Vas començar aquest curs amb nocions de JavaScript. L'acabes sabent dissenyar un contracte abans d'escriure una línia, construir una API per capes amb validació, persistència transaccional i autenticació, protegir-la, posar-la a la memòria cau, mesurar-la, documentar-la, provar-la, empaquetar-la i desplegar-la, i —el més difícil— sostenir-la mentre canvia sense deixar penjat qui en depèn. Això no és poca cosa: és la feina completa.
Ara ves a construir Aula Aroma. I quan acabis, llegeix-la com si fossis l'integrador que arribarà d'aquí a tres anys.
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
