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

  1. L'encàrrec: l'API d'Aula Aroma
  2. Dominis alternatius
  3. Lliurament per fases i criteris d'acceptació
  4. Rúbrica d'autoavaluació
  5. Guia d'arrencada: el primer dia
  6. Pla de treball per setmanes
  7. Pistes per als punts on gairebé tothom s'encalla
  8. Solució de referència de la Fase 2
  9. Errors comuns i consells
  10. Exercicis
  11. Conclusió del curs

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

  1. 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.
  2. Tancament d'inscripcions. No s'admeten inscripcions noves a partir de X hores abans del començament (parametritzable per curs; per defecte 24).
  3. 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.
  4. 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 %.
  5. Valoració condicionada. Un assistent només pot valorar una sessió si la seva inscripció figura com a assistida, i només una vegada.
  6. 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 un SELECT previ.
  • 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?

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

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

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

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

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

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

GET /v1/sessions?des=2027-03-27T22:00:00Z&fins=2027-03-28T22:00:00Z&seu=mad_centre

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

  1. 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.yaml escrit 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 SELECT previ. 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 DELETE per 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.

  1. La llista d'espera d'una sessió.
  2. El descompte del 15 % per als clients de la botiga.
  3. La valoració mitjana d'un instructor.
  4. La cancel·lació d'una inscripció.
  5. 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ó:

  1. POST /v1/valoracions amb "puntuacio": 0.
  2. POST /v1/sessions/ses_0412/inscripcions en una sessió amb 12 de 12 places.
  3. PUT /v1/inscripcions/ins_555/cancellacio quan ins_555 pertany a una altra persona.
  4. POST /v1/sessions/ses_9999/inscripcions sense que existeixi ses_9999.
  5. POST /v1/sessions/ses_0412/inscripcions 6 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

  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 és estat: "en_espera".
  2. (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.
  3. (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.
  4. (b) Subrecurs de la inscripció amb PUT: té efectes propis (reemborsament), resultat per retornar i és idempotent. Un DELETE perdria la informació.
  5. (d) Paràmetres de consulta des i fins sobre GET /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

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats