Escena Viva ja té routers, controladors i middleware propi. Si la desplegessis avui funcionaria, i també funcionaria qualsevol que li volgués fer mal: no envia capçaleres de seguretat, no controla des de quin origen la criden, no registra les peticions en un format estàndard, no comprimeix res i no impedeix que un script compri les 1189 entrades lliures del catàleg en un bucle de deu segons. En aquesta lliçó muntem el kit mínim: cinc paquets, un més de mencionat, i per a cadascun la mateixa tríada —quin problema resol, com es configura i què passa si no el poses—, tots passats abans pel filtre d'auditoria del mòdul 5.

Contingut

  1. El criteri del mòdul 5 aplicat a cada paquet
  2. helmet: capçaleres de seguretat
  3. cors: peticions des d'un altre origen
  4. morgan: registre HTTP
  5. compression: amplada de banda
  6. express-rate-limit: límits d'ús
  7. cookie-parser: preparant el mòdul 8
  8. L'ordre correcte: crearAplicacio() completa

  1. El criteri del mòdul 5 aplicat a cada paquet

Abans d'escriure npm install, la comprovació que ja saps fer:

npm view helmet version license repository.url maintainers  # metadades basiques
npm view helmet time.modified   # un paquet de seguretat sense publicar en tres anys alarma
npm view helmet dependencies    # cada dependencia es superficie d'atac
npm audit                       # despres d'installar: vulnerabilitats conegudes

Resultat de l'anàlisi per als paquets d'aquesta lliçó:

Paquet Dependències pròpies Manteniment Veredicte
helmet Cap Actiu, versió major recent Acceptar
cors 2 (object-assign, vary) Estable, canvis poc freqüents Acceptar amb vigilància
morgan i compression 4 i 5, totes de l'equip d'Express Estables i actius Acceptar
express-rate-limit Cap Molt actiu, bona documentació Acceptar
cookie-parser 2 Estable Acceptar (mòdul 8)

Que helmet i express-rate-limit tinguin zero dependències és exactament el tipus de dada que buscaves al mòdul 5: menys codi de tercers, menys superfície, menys risc que un mantenidor compromès acabi al teu node_modules. I com sempre, compte amb el typosquatting: els paquets es diuen helmet (no helmetjs), cors (no express-cors) i express-rate-limit (no express-ratelimit). Instal·la'ls amb npm install helmet cors morgan compression express-rate-limit i comprova després amb npm ls --all --parseable | wc -l quant ha crescut l'arbre i amb npm audit si alguna cosa ve amb vulnerabilitats.

  1. helmet: capçaleres de seguretat

Quin problema resol. Els navegadors implementen una desena de mecanismes de defensa que només s'activen si el servidor els demana amb capçaleres; sense elles es comporten de la manera més permissiva possible. N'hi ha prou amb aplicacio.use(helmet()), i aquestes són les capçaleres que apareixen:

Capçalera Valor per defecte (resumit) Què evita
Content-Security-Policy default-src 'self'; script-src 'self'; ... Que s'executi JavaScript d'orígens no autoritzats (la defensa real contra XSS)
X-Content-Type-Options nosniff Que el navegador endevini el tipus d'un fitxer i interpreti com a script una cosa que no ho és
Strict-Transport-Security max-age=31536000; includeSubDomains Que el navegador faci servir HTTP després de la primera visita per HTTPS
X-Frame-Options SAMEORIGIN Que la teva pàgina es carregui en un iframe aliè (clickjacking)
Referrer-Policy no-referrer Filtrar la URL completa d'origen a llocs externs
Cross-Origin-Opener-Policy i -Resource-Policy same-origin Aïllar la teva finestra d'altres pestanyes i evitar que altres llocs incrustin els teus recursos
Origin-Agent-Cluster i X-DNS-Prefetch-Control ?1 / off Aïllament de processos i resolució DNS anticipada que filtra navegació
X-Permitted-Cross-Domain-Policies none Polítiques heretades de Flash/PDF

Què passa si no el poses: totes aquestes defenses queden desactivades. Cap no és imprescindible per si sola ni substitueix validar l'entrada, però juntes converteixen diversos atacs possibles en impossibles per una línia de codi.

La CSP i el teu front-end estàtic

Aquí hi ha el problema pràctic. La CSP per defecte de helmet inclou script-src 'self', cosa que bloqueja tot el JavaScript en línia. Si public/index.html té un <button onclick="comprar('ses-001-1')"> o un <script> amb constants de les sales, totes dues coses deixaran de funcionar i a la consola del navegador apareixerà Refused to execute inline script because it violates the following Content-Security-Policy directive. La reacció instintiva és desactivar la CSP. No ho facis: és l'única capçalera de la llista que de debò atura un XSS. Les sortides correctes, per ordre de preferència: treu el JavaScript en línia a public/app.js i fes servir addEventListener en comptes d'onclick (una hora de feina i arregla el problema per sempre); si algun script en línia és inevitable, fes servir un nonce per resposta; i només com a últim recurs relaxa la directiva concreta, mai la CSP sencera.

// Opcio 2: nonce per peticio, generat abans de helmet.
const { randomBytes } = require('node:crypto');
aplicacio.use((peticio, resposta, seguent) => {
  resposta.locals.nonce = randomBytes(16).toString('base64');
  seguent();
});

aplicacio.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      // El nonce es recalcula a cada resposta: no es reutilitzable.
      scriptSrc: ["'self'", (peticio, resposta) => `'nonce-${resposta.locals.nonce}'`],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:'],
      connectSrc: ["'self'"], // des d'on pot fer fetch el front-end
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
  },
  // Nomes te sentit si de debo serveixes per HTTPS.
  strictTransportSecurity: configuracio.esProduccio
    ? { maxAge: 31_536_000, includeSubDomains: true }
    : false,
}));

API pura sense front-end: si Escena Viva només servís JSON, la CSP seria gairebé irrellevant i podries deixar helmet() per defecte. El conflicte apareix perquè servim public/.

  1. cors: peticions des d'un altre origen

Quin problema resol. El navegador aplica la política del mateix origen: una pàgina només llegeix respostes del seu mateix origen (esquema + host + port).

Pàgina API Mateix origen? Motiu
http://localhost:3000 http://localhost:3000 Sí Idèntics
http://localhost:5173 http://localhost:3000 No Port diferent
https://escenaviva.test http://escenaviva.test No Esquema diferent
https://escenaviva.test https://api.escenaviva.test No Host (subdomini) diferent

Quan no coincideixen, el navegador fa la petició però amaga la resposta al JavaScript tret que el servidor ho autoritzi amb Access-Control-Allow-Origin. Compte amb el matís: CORS no protegeix el teu servidor, protegeix l'usuari del navegador; un curl o un script en Node l'ignoren del tot.

La petició de verificació prèvia (preflight)

Per a peticions «no simples» —qualsevol POST amb Content-Type: application/json, o amb capçaleres personalitzades com X-Id-Peticio— el navegador envia abans un OPTIONS demanant permís:

OPTIONS /api/comandes HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-id-peticio

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: content-type,x-id-peticio
Access-Control-Max-Age: 600

I només si aquesta resposta autoritza, el navegador envia el POST real. Access-Control-Max-Age és important per al rendiment: sense això, el navegador repeteix l'OPTIONS abans de cada petició.

Configuració real d'Escena Viva

// src/middleware/cors.js
const cors = require('cors');
const { configuracio } = require('../config/index.js');
function crearCors() {
  return cors({
    // Llista blanca llegida d'ORIGENS_PERMESOS (06-02), mai '*'.
    origin(origen, devolucio) {
      // Sense capcalera Origin (curl, apps mobils, servidor a servidor) s'accepta.
      if (!origen || configuracio.origensPermesos.includes(origen)) {
        devolucio(null, true);
        return;
      }
      const missatge = `Origen no permes: ${origen}`;
      devolucio(Object.assign(new Error(missatge), { codi: 'RUTA_NO_PERMESA' }));
    },
    methods: ['GET', 'POST', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Accept', 'X-Id-Peticio', 'X-Canal-Venda'],
    // exposedHeaders: les que el JavaScript del navegador podra LLEGIR.
    exposedHeaders: ['X-Id-Peticio', 'RateLimit-Remaining'],
    maxAge: 600,
    credentials: false,
  });
}

origin: '*' enfront de llista blanca

origin: '*' Llista blanca
Qui pot cridar des d'un navegador Qualsevol lloc web del món Només els que autoritzes
Compatible amb credentials: true No: el navegador ho rebutja Sí
Risc i quan és acceptable Un web maliciós pot llegir les teves respostes amb la sessió del visitant; només per a API pública de només lectura sense dades personals Acotat; per a tota la resta

L'advertiment sobre credentials. Si actives credentials: true, el navegador envia galetes i capçaleres d'autenticació a les peticions entre orígens, cosa que obre la porta que un altre web actuï en nom de l'usuari amb sessió oberta. Tres regles si algun dia ho necessites (mòdul 8): credentials: true exigeix un origen concret, perquè amb '*' el navegador rebutja la resposta; no retornis mai dinàmicament l'Origin rebut sense comprovar-lo contra la llista blanca, perquè això és una llista blanca falsa que accepta tothom; i combina-ho amb galetes SameSite=Lax o Strict i protecció CSRF. Què passa si no el poses: el public/app.js servit des d'un altre port rebrà el clàssic blocked by CORS policy i no veurà ni una resposta. Si en producció el front-end se serveix des del mateix origen que l'API, CORS no cal: és l'escenari més segur.

  1. morgan: registre HTTP

Quin problema resol. Deixa constància de cada petició en un format estàndard que les eines d'anàlisi saben llegir. A 06-04 vas escriure el teu propi registre; morgan fa el mateix amb formats consolidats i tokens propis.

aplicacio.use(morgan('dev'));      // desenvolupament: compacte i acolorit per estat
// GET /api/esdeveniments/evt-001 200 412 - 3.201 ms
aplicacio.use(morgan('combined')); // produccio: Combined Log Format (Apache/Nginx)
// 127.0.0.1 - - [14/Aug/2026:09:12:44 +0000] "GET /api/esdeveniments HTTP/1.1" 200 812 "-" "curl/8.5.0"
Format Contingut Per a què
dev Mètode, ruta, estat acolorit, temps Desenvolupament
combined Format Apache amb Referer i User-Agent Producció amb eines clàssiques
common / tiny Sense Referer ni User-Agent / el mínim Alternatives més lleugeres
Personalitzat Els tokens que defineixis Registre estructurat (mòdul 11)

Enllaçar-lo amb el registre estructurat del mòdul 11

En producció els registres es consulten amb eines que necessiten JSON per línia, no text llegible; morgan ho permet definint tokens i passant-li un stream propi:

// src/middleware/registre-http.js
const morgan = require('morgan');
const { configuracio } = require('../config/index.js');
// Tokens propis: exposen dades que morgan no coneix.
morgan.token('id-peticio', (peticio) => peticio.idPeticio ?? '-');
morgan.token('canal', (peticio) => peticio.get('X-Canal-Venda') ?? '-');

// Formatador que emet una linia de JSON per peticio.
function formatJson(t, p, r) {
  return JSON.stringify({
    instant: new Date().toISOString(),
    idPeticio: t['id-peticio'](p, r),
    metode: t.method(p, r),
    ruta: t.url(p, r),
    estat: Number(t.status(p, r)),
    bytes: Number(t.res(p, r, 'content-length') ?? 0),
    duracioMs: Number(t['response-time'](p, r)),
    canal: t.canal(p, r),
  });
}

function crearRegistreHttp() {
  if (!configuracio.esProduccio) {
    return morgan('dev', { skip: (peticio) => peticio.path === '/api/salut' });
  }
  // Diagnostics per stderr, com marca la convencio del curs.
  return morgan(formatJson, { stream: { write: (linia) => process.stderr.write(linia) } });
}

Amb això, morgan substitueix crearRegistrePeticions de 06-04 (conserva el teu si t'agrada més: fan el mateix, i ara entens per dins el de tercers). L'opció skip evita omplir els registres amb els sondejos de salut de l'orquestrador. Què passa si no el poses: quan alguna cosa falli en producció no tindràs ni idea de què va demanar el client, ni quan, ni quant va trigar. Depurar a cegues.

  1. compression: amplada de banda

Quin problema resol. El JSON comprimeix extraordinàriament bé: el catàleg complet d'Escena Viva pot passar de 12 KB a menys de 2 KB, i menys bytes és menys temps de càrrega i menys cost de trànsit.

aplicacio.use(compression({
  threshold: 1024, // per sota no compensa: la CPU costa mes que els bytes
  level: 6,        // equilibri habitual entre CPU i ratio
  // Permet al client desactivar-la amb la capcalera X-Sense-Compressio.
  filter(peticio, resposta) {
    if (peticio.headers['x-sense-compressio']) return false;
    return compression.filter(peticio, resposta);
  },
}));
Què comprimeix bé Què no s'ha de comprimir
JSON, HTML, CSS, JavaScript, SVG, CSV JPEG, PNG, WebP (ja comprimits)
Els informes d'ocupació del mòdul 3 MP4, MP3, ZIP, gzip
Respostes grans de l'API Respostes per sota del llindar

Comprimir el que ja està comprimit gasta CPU i de vegades augmenta la mida. El filter per defecte de compression ja consulta la taula de tipus MIME i evita aquests casos.

Per què de vegades es delega al proxy invers

En molts desplegaments (mòdul 11) la compressió la fa Nginx o el CDN, no Node. Comprimir a Node funciona en qualsevol desplegament sense configuració externa i és imprescindible si no hi ha proxy al davant; comprimir al proxy allibera CPU del procés i sol portar brotli optimitzat i memòria cau de respostes comprimides, però exigeix control d'aquest proxy.

Regla pràctica: activa-la a Node per defecte; si més endavant mesures que la CPU és el coll d'ampolla i hi ha un proxy al davant, desactiva-la allà, però mai als dos llocs alhora. Per comprovar-la, compara curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' localhost:3000/api/esdeveniments amb la mateixa crida sense la capçalera. Què passa si no el poses: no es trenca res; simplement envies tres o quatre vegades més bytes dels necessaris a cada resposta.

  1. express-rate-limit: límits d'ús

Quin problema resol. Res no impedeix que un script cridi POST /api/comandes en bucle: amb 1189 entrades lliures al catàleg i una petició cada 20 ms, l'Auditorio Ribera es queda sense aforament en menys de mig minut. També protegeix de la força bruta contra l'inici de sessió del mòdul 8.

// src/middleware/limits.js
const { rateLimit } = require('express-rate-limit');
const cos429 = (m) => ({ error: { codi: 'MASSA_PETICIONS', missatge: m, estat: 429 } });

/** Limit general de l'API: generos, nomes frena abusos evidents. */
const limitGeneral = rateLimit({
  windowMs: 15 * 60 * 1000,   // finestra de 15 minuts
  limit: 300,                 // 300 peticions per IP i finestra
  standardHeaders: 'draft-7', // capcaleres RateLimit-* estandard
  legacyHeaders: false,       // sense les antigues X-RateLimit-*
  message: cos429('Has superat el limit de peticions. Torna-ho a provar mes tard.'),
});

/** Limit estricte per a la compra: es l'operacio que consumeix aforament. */
const limitCompra = rateLimit({
  windowMs: 60 * 1000, // 1 minut
  limit: 5,            // 5 comandes per IP i minut
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  // Els rebutjats per aforament tambe compten: si no, un bucle d'intents
  // fallits quedaria sense limit.
  skipFailedRequests: false,
  message: cos429('Massa intents de compra. Espera un minut.'),
});

// Aplicacio: general a tota l'API, estricte nomes a la compra.
api.use(limitGeneral);
rutesComandes.post('/', limitCompra, crearComanda);

En superar-lo, el client rep un 429 Too Many Requests amb RateLimit-Limit: 5, RateLimit-Remaining: 0, RateLimit-Reset: 43 i Retry-After: 43. RateLimit-Remaining li diu quantes peticions li queden i Retry-After quants segons ha d'esperar; un client ben fet les respecta en comptes de reintentar a cegues.

Dues limitacions que has de conèixer ara. Primera: el magatzem per defecte és la memòria del procés, de manera que amb diversos processos (cluster del mòdul 10) o diverses instàncies (mòdul 11) cadascun porta el seu compte i el límit real es multiplica; la solució és un magatzem compartit a Redis (mòdul 10). Segona: depèn de req.ip, que al seu torn depèn de trust proxy; mal configurat, o tots els clients comparteixen la IP del proxy i es bloquegen entre ells, o qualsevol falsifica la seva amb una capçalera. Al mòdul 8 hi aprofundirem amb límits per usuari autenticat, finestres lliscants i retard progressiu. Què passa si no el poses: un sol client pot exhaurir l'aforament, saturar el procés o provar contrasenyes sense límit.

  1. cookie-parser: preparant el mòdul 8

aplicacio.use(cookieParser(configuracio.clauPassarella)) interpreta la capçalera Cookie i deixa les galetes a req.cookies (i les signades a req.signedCookies). Escena Viva encara no ho necessita: l'API és anònima, no hi ha sessió i cap endpoint no depèn de qui ets.

Ho deixem anunciat perquè al mòdul 8 apareixeran les sessions amb Passport i les galetes httpOnly, secure i SameSite, i llavors aquest middleware serà el primer de la llista. Instal·lar-lo ara «per si de cas» seria afegir superfície sense necessitat: la disciplina del mòdul 5 també consisteix a no instal·lar el que no fas servir.

  1. L'ordre correcte: crearAplicacio() completa

Aquest és el codi final del mòdul, comentat línia a línia. L'ordre no és arbitrari: cada posició té un motiu.

// src/app.js — versio completa del modul 6.
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const { configuracio } = require('./config/index.js');
const { crearCors } = require('./middleware/cors.js');
const { crearRegistreHttp } = require('./middleware/registre-http.js');
const { idPeticio } = require('./middleware/id-peticio.js');
const { limitGeneral } = require('./middleware/limits.js');
const { crearRutesApi } = require('./rutes/index.js');
// rutaNoTrobada i gestorDErrors arriben a 06-07.
const { rutaNoTrobada } = require('./middleware/no-trobat.js');
const { gestorDErrors } = require('./middleware/errors.js');

function crearAplicacio({ servirEstatics = true, registrar = true, limitar = true } = {}) {
  const aplicacio = express();

  // 0. AJUSTOS, abans que res: el limitador necessita 'trust proxy' per a req.ip.
  aplicacio.disable('x-powered-by');
  aplicacio.set('trust proxy', configuracio.confiarEnProxy);
  aplicacio.set('case sensitive routing', true);
  aplicacio.set('json spaces', configuracio.esProduccio ? 0 : 2);

  // 1. IDENTIFICADOR: el primer, perque el registre, els errors i qualsevol
  //    traca posterior el faran servir.
  aplicacio.use(idPeticio);

  // 2. SEGURETAT DE CAPCALERES: com mes aviat millor, perque TOTES les respostes
  //    les portin, incloses les d'error i les dels estatics.
  aplicacio.use(helmet({
    contentSecurityPolicy: politicaCsp(),
    strictTransportSecurity: configuracio.esProduccio,
  }));

  // 3. CORS abans de les rutes i dels limits: l'OPTIONS de verificacio
  //    previa s'ha de respondre sense consumir quota.
  aplicacio.use(crearCors());

  // 4. REGISTRE: despres de l'identificador (per imprimir-lo) i abans de tot el
  //    que pugui respondre, perque no s'escapi cap peticio.
  if (registrar) aplicacio.use(crearRegistreHttp());
  aplicacio.use(compression({ threshold: 1024 })); // 5. COMPRESSIO

  // 6. LIMITS abans del COS (7): rebutjar una peticio abusiva es mes
  //    barat si no has analitzat 100 KB de JSON.
  if (limitar) aplicacio.use('/api', limitGeneral);
  aplicacio.use(express.json({ limit: configuracio.limitCos }));

  // 8. ESTATICS: rutes disjuntes de l'API; amb fallthrough, un fitxer
  //    inexistent continua cap a ella.
  const estatics = { index: 'index.html', dotfiles: 'ignore', maxAge: '1h' };
  if (servirEstatics) aplicacio.use(express.static(configuracio.directoriPublic, estatics));

  aplicacio.use('/api', crearRutesApi()); // 9. L'API: els routers de 06-03.
  aplicacio.use(rutaNoTrobada);           // 10. 404: encaixa amb el no ates.
  aplicacio.use(gestorDErrors);           // 11. ERRORS: SEMPRE l'ultim.

  return aplicacio;
}

Les set regles d'ordre, per memoritzar-les: identificador primer (tota la resta el referencia); seguretat aviat (que les capçaleres arribin també a errors i estàtics); CORS abans dels límits (l'OPTIONS previ no ha de consumir quota); límits abans del cos (no gastar CPU analitzant el que rebutjaràs); cos abans de les rutes (req.body no existeix si no); 404 després de les rutes (només captura el no atès); i errors l'últim (només veu el registrat abans que ell). I els indicadors registrar i limitar no són cap caprici: al mòdul 9 els posaràs a false perquè les proves no omplin la sortida de registres ni fallin en superar el límit a la petició número 301.

Errors Comuns i Consells

  • Desactivar la CSP sencera perquè trenca el front-end. És llençar l'única defensa real contra XSS. Treu el JavaScript en línia a un fitxer o fes servir nonces.
  • origin: '*' amb credentials: true. El navegador rebutja la combinació. I si «ho arregles» retornant l'Origin rebut sense comprovar-lo, has construït una llista blanca que accepta tothom.
  • Creure que CORS protegeix el servidor. Protegeix l'usuari del navegador; l'autorització de debò arriba al mòdul 8.
  • Posar el limitador després d'express.json() (analitzes 100 KB abans de rebutjar) o fer-lo servir amb memòria i diversos processos (amb quatre treballadors el límit real és quàdruple; Redis al mòdul 10).
  • Comprimir imatges o vídeo, o comprimir a Node i al proxy alhora. Gastes CPU per no guanyar bytes: deixa el filter per defecte i tria un sol lloc.
  • Instal·lar cookie-parser sense fer servir galetes. Superfície de franc: instal·la'l quan el necessitis.
  • morgan('dev') en producció. Els codis de color són seqüències d'escapament que embruten els fitxers de registre; fes servir combined o el teu formatador JSON.
  • Consell: afegeix npm audit --audit-level=high a l'script comprovar. Cadascun d'aquests paquets és codi de tercers que s'executa a cada petició.

Exercicis

Exercici 1: auditar abans d'instal·lar

Per als cinc paquets d'aquesta lliçó, construeix una taula amb: versió actual, llicència, data de l'última publicació, nombre de dependències directes i si apareix a npm audit. Decideix justificadament si acceptaries cadascun en un projecte real i quina alternativa buscaries si algun portés dos anys sense publicar.

Exercici 2: CORS amb dos orígens

Configura cors perquè accepti http://localhost:5173 i https://escenaviva.test i rebutgi la resta. Demostra amb curl els tres casos: la petició de verificació prèvia OPTIONS des d'un origen permès (ha de retornar les capçaleres Access-Control-Allow-*), un GET des d'un origen no permès (no ha de portar Access-Control-Allow-Origin) i un GET sense capçalera Origin (ha de funcionar amb normalitat).

Exercici 3: el límit que salva l'aforament

Aplica limitCompra a POST /api/comandes amb 5 peticions per minut. Escriu un script que llanci 8 comandes seguides d'1 entrada per a ses-001-1 i imprimeixi l'estat i les capçaleres RateLimit-* de cadascuna. Comprova que les tres últimes retornen 429 amb Retry-After i que el cos respecta el format { error: { codi, missatge, estat } }.

Solucions

Solució 1

for p in helmet cors morgan compression express-rate-limit; do
  echo "== $p"; npm view "$p" version license time.modified dependencies
done && npm audit --audit-level=moderate

Criteris que hauries d'haver aplicat: llicència permissiva, publicació en els últims 12-18 mesos, poques dependències directes i de mantenidors recognoscibles. Si cors portés dos anys sense publicar, la resposta raonable no és abandonar-lo —és un paquet petit i estable— sinó vigilar les seves incidències obertes i estar disposat a substituir-lo per un middleware propi de vint línies, perquè CORS al final són quatre capçaleres.

Solució 2

# 1. Verificacio previa des d'un origen permes -> 204 amb
#    Access-Control-Allow-Origin, -Allow-Methods i -Max-Age.
curl -si -X OPTIONS http://localhost:3000/api/comandes -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type'

# 2. GET des d'un origen NO permes: el grep no retorna res, aixi que el
#    navegador amagaria la resposta.
curl -si http://localhost:3000/api/esdeveniments -H 'Origin: https://malicios.test' \
  | grep -i 'access-control-allow-origin'

# 3. Sense capcalera Origin (curl normal, servidor a servidor): 200.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/esdeveniments

El tercer cas demostra el punt clau: CORS és una defensa del navegador, no del servidor.

Solució 3

// provar-limit.js
const COMANDA = { sessioId: 'ses-001-1', quantitat: 1, correu: '[email protected]', canal: 'web' };

async function principal() {
  for (let intent = 1; intent <= 8; intent += 1) {
    const r = await fetch('http://localhost:3000/api/comandes', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(COMANDA),
    });
    console.log(JSON.stringify({
      intent,
      estat: r.status,
      restants: r.headers.get('RateLimit-Remaining'),
      reintentarEn: r.headers.get('Retry-After'),
    }));
  }
}

principal().catch((error) => {
  console.error('[provar-limit]', error.message);
  process.exitCode = 1;
});

Sortida esperada: els intents 1 a 5 retornen 201 amb RateLimit-Remaining baixant de 4 a 0; el 6, el 7 i el 8 retornen 429 amb Retry-After: 52. Sense el limitador, tots vuit haurien venut entrada; amb l'aforament de ses-001-1, un bucle sense fre l'exhaureix en segons.

Conclusió

Escena Viva ja surt amb el kit mínim d'una API en producció. helmet afegeix una desena de capçaleres de seguretat per una línia, amb el matís important que la seva CSP obliga a treure el JavaScript en línia del front-end —i aquesta és la solució correcta, no desactivar-la—. cors autoritza public/app.js a cridar l'API des d'un altre origen mitjançant una llista blanca llegida de la configuració, mai amb '*', i ara saps què és una petició de verificació prèvia i per què credentials mereix respecte. morgan registra cada petició en format estàndard i, amb tokens propis, en JSON per línia a punt per al mòdul 11. compression retalla la mida de les respostes quan compensa. I express-rate-limit impedeix que un script exhaureixi l'aforament, amb l'advertiment que el seu magatzem en memòria no sobreviu a diversos processos.

Sobretot, has vist la versió completa de crearAplicacio() amb les set regles d'ordre raonades: identificador primer, seguretat aviat, CORS abans dels límits, límits abans del cos, cos abans de les rutes, 404 després de les rutes i errors l'últim. Però queda un forat molt gran: POST /api/comandes continua fent registrarComanda(peticio.body) amb el que vingui al cos —una quantitat de -5, un sessioId que és un objecte, un correu de 40.000 caràcters—, i cap middleware d'aquesta lliçó no mira el contingut de les dades.

A la lliçó següent, Validació de Dades d'Entrada, tanquem aquest forat: per què no es confia mai en el client encara que el formulari validi, on ha de viure la validació (a la vora, no al domini), els esquemes de zod per a les comandes d'Escena Viva, un middleware genèric validar(esquema, origen) que deixa el resultat a req.dadesValidades —i que respecta que req.query sigui de només lectura—, i com retornar un 400 que de debò ajudi qui crida l'API sense revelar el que no ha de revelar.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats