Ja sabem llegir una petició i construir una resposta correcta. Falta la decisió que hi ha al mig: quin codi atén cada petició. Això és enrutar.

Un enrutador no és més que una funció que, donada una parella (mètode, ruta), troba el gestor que li correspon. Sona trivial, i el primer intent cap en deu línies d'if/else. El problema és que aquest primer intent s'ensorra tan bon punt apareix la primera ruta amb un identificador a dins, /esdeveniments/evt-001, i a partir d'aquí cada ruta nova empitjora el desordre. En aquesta lliçó construïm un enrutador de veritat: una taula de rutes, patrons tipus /esdeveniments/:id compilats a expressions regulars, extracció de paràmetres, 404 quan la ruta no existeix, 405 amb capçalera Allow quan existeix però amb un altre mètode, i un try/catch central que tradueix els nostres error.codi a estats HTTP fent servir la taula de la lliçó anterior. És la primera vegada que apareix la idea de gestor d'errors centralitzat, que Express formalitzarà a la lliçó 06-07.

Contingut

  1. Què és enrutar exactament
  2. L'intent ingenu: if/else sobre req.url
  3. On es trenca: rutes amb paràmetres
  4. La taula de rutes
  5. compilarPatro: de /esdeveniments/:id a expressió regular
  6. Normalitzar la ruta abans de casar-la
  7. Casar, extreure paràmetres i respondre 404 o 405
  8. Despatx asíncron i gestió d'errors centralitzada
  9. Les rutes de l'API d'Escena Viva
  10. Per què a partir del Mòdul 6 farem servir Express

  1. Què és enrutar exactament

Enrutar és casar una parella (mètode, ruta) amb un gestor. Res més. Tota la complexitat dels enrutadors reals —Express, Fastify, Koa— neix de tres exigències afegides, i un enrutador que les resolgui ja és útil de veritat:

  • Els patrons tenen parts variables. /esdeveniments/evt-001 i /esdeveniments/evt-002 han d'anar al mateix gestor, que necessita saber quin dels dos li ha tocat.
  • El mètode forma part de la identitat de la ruta. GET /comandes (llistar) i POST /comandes (crear) són coses diferents amb el mateix camí.
  • Les fallades s'han de distingir. No és el mateix "aquesta ruta no existeix" (404) que "aquesta ruta existeix, però no amb aquest mètode" (405).

  1. L'intent ingenu: if/else sobre req.url

Així comença tothom, i per a dues rutes està perfectament bé:

async function gestionarPeticio(req, res) {
  if (req.method === 'GET' && req.url === '/salut') return respondreJson(res, 200, { estat: 'ok' });
  if (req.method === 'GET' && req.url === '/esdeveniments') return respondreJson(res, 200, await obtenirCataleg());
  return respondreError(res, 404, 'Ruta no trobada', 'RECURS_NO_TROBAT');
}

Fixa't en els return: són la disciplina de la lliçó anterior, la que evita ERR_HTTP_HEADERS_SENT. I aquest codi ja té una fallada silenciosa: compara amb req.url en comptes de comparar amb el pathname. GET /esdeveniments?sala=Sala%20B%C3%B3veda no casa amb '/esdeveniments', perquè la cadena inclou la consulta. El resultat és un 404 desconcertant que apareix només quan algú afegeix un filtre. La correcció és la de 04-02: analitzar amb new URL i comparar contra url.pathname.

  1. On es trenca: rutes amb paràmetres

Ara afegeix GET /esdeveniments/evt-001. Com que l'identificador és variable, la comparació per igualtat ja no serveix, i apareix el trossejat manual:

// El cami cap al desastre
const parts = url.pathname.split('/');            // ['', 'esdeveniments', 'evt-001']

if (req.method === 'GET' && parts[1] === 'esdeveniments' && parts.length === 3) {
  return gestionarEsdeveniment(req, res, parts[2]);
}
if (req.method === 'GET' && parts[1] === 'esdeveniments' && parts[3] === 'sessions') {
  return gestionarSessions(req, res, parts[2]);
}

Amb quatre rutes és suportable. Amb vint és il·legible, i pel camí et trobaràs amb problemes molt concrets:

Problema Símptoma
Índexs màgics (parts[2]) Ningú recorda què és el 2; afegir un prefix /api ho trenca tot
Condicions de longitud parts.length === 3 s'oblida i /esdeveniments/x/y/z casa per accident
El mètode es repeteix a cada branca Un DELETE /esdeveniments cau al 404 genèric en comptes de donar 405
Sense descodificar, i ordre fràgil Un %20 arriba amb el percentatge posat; moure un if deixa una ruta inabastable

La solució no és escriure millors if: és separar la declaració de les rutes de la lògica que les casa. Declares una taula; un motor genèric la recorre.

  1. La taula de rutes

Una ruta és un objecte amb tres camps —{ metode: 'GET', patro: '/esdeveniments/:id', gestor: obtenirEsdeveniment }— i l'enrutador és una llista d'aquests objectes més dues operacions: registrar i despatxar.

Amb aquesta forma, afegir una ruta és afegir una fila, i el motor no canvia mai. Aquest és el camí complet d'una petició, i tot el que ve a continuació és omplir aquestes caixes:

flowchart LR
  A[request] --> B[normalitzar ruta]
  B --> C{cercar a la taula}
  C -->|casa metode i patro| D[gestor amb parametres]
  C -->|casa patro, no metode| E[405 + Allow]
  C -->|no casa res| F[404]
  D -->|llanca error| G[try/catch central]
  G --> H[estatPerError]

  1. compilarPatro: de /esdeveniments/:id a expressió regular

Necessitem convertir la cadena '/esdeveniments/:id' en alguna cosa que sàpiga dir si '/esdeveniments/evt-001' casa i, a més, extreure'n evt-001. L'eina natural és una expressió regular amb grups de captura. Es construeix en tres passos:

// Compila un patro tipus '/esdeveniments/:id/sessions' a una expressio regular
// i la llista ordenada de noms dels seus parametres.
function compilarPatro(patro) {
  const noms = [];

  // Pas 1: escapar els caracters amb significat especial en una regex.
  // Un patro com '/informes/2026.json' te un punt que ha de ser literal.
  const escapat = patro.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

  // Pas 2: substituir cada :nom per un grup de captura d'UN segment.
  const font = escapat.replace(/:([a-zA-Z][a-zA-Z0-9]*)/g, (_, nom) => {
    noms.push(nom);
    return '([^/]+)';
  });

  // Pas 3: ancorar. Sense ^ i $ la ruta casaria per trossos.
  return { expressio: new RegExp(`^${font}$`), noms };
}

Val la pena llegir a poc a poc l'expressió resultant. Per a '/esdeveniments/:id/sessions' obtenim:

compilarPatro('/esdeveniments/:id/sessions');
// { expressio: /^\/esdeveniments\/([^\/]+)\/sessions$/, noms: ['id'] }

Peça a peça:

Fragment Què significa
^ … $ La ruta comença i acaba aquí: no val casar pel mig
\/esdeveniments\/ Text literal (la barra no necessita escapament amb new RegExp, però no molesta)
( … ) Grup de captura: el que casi aquí es podrà recuperar després
[^/]+ Un o més caràcters que no siguin barra: mai no creua a un altre segment

Els dos detalls crítics són [^/]+ i les àncores. [^/]+ en comptes de .+: amb .+, el patró /esdeveniments/:id casaria amb /esdeveniments/evt-001/sessions i id valdria 'evt-001/sessions', atropellant una altra ruta. I sense ^ i $, /esdeveniments casaria dins de /admin/esdeveniments/esborrar, cosa que a més d'un error funcional és un forat de seguretat.

També importa + en comptes de *: amb *, /esdeveniments/ (barra final, sense identificador) casaria amb id valent cadena buida, i acabaries buscant l'esdeveniment ''.

Amb el patró compilat, casar una ruta i extreure'n els paràmetres és directe:

function aparellar(compilat, ruta) {
  const coincidencia = compilat.expressio.exec(ruta);
  if (coincidencia === null) return null;

  const parametres = {};
  compilat.noms.forEach((nom, index) => {
    // El grup 0 es la coincidencia completa; els parametres comencen a l'1.
    // Es descodifica AQUI, un segment cada cop (mira la llico 04-02).
    parametres[nom] = decodeURIComponent(coincidencia[index + 1]);
  });

  return parametres;
}

Tornar null davant de {} és deliberat: un patró sense paràmetres que casa torna un objecte buit, que és diferent de "no casa". Confondre les dues coses amb un if (!parametres) seria un error, perquè {} és un valor verdader i null no.

  1. Normalitzar la ruta abans de casar-la

La mateixa pàgina es pot demanar de diverses maneres, i totes han de dur al mateix lloc:

Petició Problema Decisió
/esdeveniments/ Barra final sobrera Treure-la (excepte a l'arrel /)
/ESDEVENIMENTS Majúscules No canviar-les: les rutes distingeixen majúscules per norma
/esdeveniments//evt-001 Barra duplicada Col·lapsar-la en una
/esdeveniments%2Fevt-001 Barra codificada No descodificar la ruta sencera: es fa per segments
/esdeveniments?sala=X Consulta inclosa Casar contra pathname, no contra req.url
// Deixa la ruta en forma canonica per poder casar-la de manera estable.
function normalitzarRuta(pathname) {
  const collapsada = pathname.replace(/\/{2,}/g, '/');
  return collapsada.length > 1 ? collapsada.replace(/\/+$/, '') : collapsada;
}

Sobre les majúscules, la norma és clara i contraintuïtiva: el domini no distingeix majúscules, la ruta sí. /Esdeveniments i /esdeveniments són recursos diferents. Passar la ruta a minúscules "per comoditat" sembla amable fins que un identificador legítim porta majúscules (EV-2026-000123) i el destrosses. Si vols ser tolerant, fes-ho amb una redirecció 301 a la forma canònica, no casant en silenci: per la mateixa raó la barra final es normalitza en comptes de registrar dues rutes, perquè un mateix contingut vivint a dues adreces confon les memòries cau i penalitza als cercadors.

  1. Casar, extreure paràmetres i respondre 404 o 405

Ja tenim totes les peces. Aquest és src/servidor/enrutador.js complet:

// src/servidor/enrutador.js
// Enrutador minim: una taula de { metode, patro, gestor } i un
// despatxador que casa (metode, ruta), extreu parametres i centralitza errors.

const { respondreError } = require('./respostes.js');

// compilarPatro, aparellar i normalitzarRuta: vistos als apartats 5 i 6.

function crearEnrutador() {
  const rutes = [];

  function registrar(metode, patro, gestor) {
    rutes.push({ metode, patro, compilat: compilarPatro(patro), gestor });
    return { registrar, get, post, despatxar };   // encadenable
  }

  const get = (patro, gestor) => registrar('GET', patro, gestor);
  const post = (patro, gestor) => registrar('POST', patro, gestor);

  async function despatxar(req, res) {
    const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
    const ruta = normalitzarRuta(url.pathname);

    // HEAD es serveix amb el gestor de GET: Node ja descarta el cos.
    const metode = req.method === 'HEAD' ? 'GET' : req.method;
    const metodesPermesos = new Set();   // per poder donar un 405 util

    for (const registre of rutes) {
      const parametres = aparellar(registre.compilat, ruta);
      if (parametres === null) continue;   // ni tan sols el cami coincideix

      metodesPermesos.add(registre.metode);
      if (registre.metode === metode) {
        return registre.gestor(req, res, { parametres, consulta: url.searchParams, url });
      }
    }

    if (metodesPermesos.size > 0) {
      // La ruta existeix: el 405 HA d'incloure Allow amb els metodes valids.
      if (metodesPermesos.has('GET')) metodesPermesos.add('HEAD');
      const permesos = [...metodesPermesos].sort().join(', ');
      return respondreError(res, 405, `Metode ${req.method} no permes a ${ruta}`,
        'METODE_NO_PERMES', { permesos }, { Allow: permesos });
    }

    return respondreError(res, 404, `No existeix la ruta ${ruta}`, 'RECURS_NO_TROBAT');
  }

  return { registrar, get, post, despatxar, rutes };
}

module.exports = { crearEnrutador, compilarPatro, aparellar, normalitzarRuta };

El 405 és la part que gairebé tothom es salta, i la norma és explícita: una resposta 405 ha d'incloure la capçalera Allow amb els mètodes acceptats. Veure-ho funcionant:

curl -i -X DELETE http://localhost:3000/esdeveniments
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: application/json; charset=utf-8

Un client que rep un 404 pensa que s'ha equivocat d'URL i es rendeix. Un que rep 405 amb Allow: GET, HEAD sap exactament què fer. És la diferència entre una API que es pot explorar i una que s'ha d'endevinar.

Sobre l'ordre i l'especificitat: el nostre enrutador recorre la taula i guanya la primera que casa, així que les rutes més específiques van primer. Amb /esdeveniments/:id i /esdeveniments/destacats registrades en aquest ordre, /esdeveniments/destacats cauria a la primera amb id = 'destacats'. Els enrutadors seriosos ordenen per especificitat; el nostre delega aquesta responsabilitat en tu, i per això convé registrar sempre els literals abans que els patrons amb paràmetres.

  1. Despatx asíncron i gestió d'errors centralitzada

Els gestors són async, perquè gairebé tots llegeixen el catàleg. Això vol dir que qualsevol pot llançar, i no volem repetir un try/catch a cadascun. El pugem una sola vegada al despatxador:

// Embolcalla el despatx: un unic punt on es tradueixen els errors.
function crearGestorHttp(enrutador) {
  return function gestionarPeticio(req, res) {
    // Promise.resolve() captura tambe el que llanci despatxar de forma sincrona.
    Promise.resolve().then(() => enrutador.despatxar(req, res)).catch((error) => {
      if (res.headersSent) {
        // Les capcaleres ja han sortit: no es pot desmentir l'estat.
        console.error("[error] despres d'enviar capcaleres:", error);
        return res.destroy();
      }
      const { estat, codi, missatge } = cosPerError(error);
      respondreError(res, estat, missatge, codi);
    });
  };
}

Això és un abans i un després en l'arquitectura del servidor:

  • Els gestors deixen de gestionar errors HTTP. Si l'esdeveniment no existeix, llancen ESDEVENIMENT_NO_TROBAT i se n'obliden. La traducció a 404 la fa cosPerError, en un sol lloc.
  • El domini continua sense conèixer HTTP. Esdeveniment.reservar llança AFORAMENT_INSUFICIENT igual que quan el cridava la CLI del Mòdul 1.
  • No es perd res. Un error sense traducció coneguda es converteix en 500 i es registra a stderr, que és exactament el que volem per assabentar-nos-en.
  • res.headersSent decideix l'últim recurs. Si la resposta ja estava en marxa (per exemple, enmig de servir un fitxer), el que és honrat és destruir la connexió: el client detectarà la resposta truncada.

Aquest patró —un embolcall que captura i tradueix— és exactament el que Express anomena gestor d'errors, amb la seva signatura de quatre arguments (err, req, res, next). El veurem a la lliçó 06-07 i en reconeixeràs la idea a l'instant, perquè l'acabes d'escriure.

  1. Les rutes de l'API d'Escena Viva

Amb el motor a punt, declarar l'API és gairebé llegir una llista:

// src/servidor/rutes-api.js
// Declaracio de les rutes HTTP d'Escena Viva. Els gestors NO
// gestionen errors: llancen amb error.codi i ja decideix l'enrutador.

const { obtenirCataleg, obtenirEsdevenimentPerId } = require('../cataleg-dades.js');
const { respondreJson } = require('./respostes.js');

const arrencada = Date.now();

async function cercarEsdevenimentObligatori(id) {
  const esdeveniment = await obtenirEsdevenimentPerId(id);
  if (!esdeveniment) {
    const error = new Error(`No existeix l'esdeveniment ${id}`);
    error.codi = 'ESDEVENIMENT_NO_TROBAT';
    throw error;
  }
  return esdeveniment;
}

function registrarRutes(enrutador) {
  enrutador.get('/salut', (req, res) => {
    const segonsEnMarxa = Math.round((Date.now() - arrencada) / 1000);
    respondreJson(res, 200, { estat: 'ok', versio: process.version, segonsEnMarxa });
  });

  enrutador.get('/esdeveniments', async (req, res, { consulta }) => {
    const sala = consulta.get('sala');
    const esdeveniments = (await obtenirCataleg())
      .filter((esdeveniment) => sala === null || esdeveniment.sala === sala);

    respondreJson(res, 200, { total: esdeveniments.length, esdeveniments });
  });

  enrutador.get('/esdeveniments/:id', async (req, res, { parametres }) => {
    respondreJson(res, 200, await cercarEsdevenimentObligatori(parametres.id));
  });

  enrutador.get('/esdeveniments/:id/sessions', async (req, res, { parametres }) => {
    const esdeveniment = await cercarEsdevenimentObligatori(parametres.id);
    respondreJson(res, 200, { esdevenimentId: esdeveniment.id, total: esdeveniment.nombreSessions, sessions: esdeveniment.sessions });
  });

  return enrutador;
}

module.exports = { registrarRutes };

I el servidor de la lliçó 04-01 se simplifica fins a una línia: http.createServer(crearGestorHttp(registrarRutes(crearEnrutador()))). Comprovació completa de l'API:

curl -s http://localhost:3000/salut                              # 200 {"estat":"ok",...}
curl -s http://localhost:3000/esdeveniments | head -3            # 200, total 3
curl -s http://localhost:3000/esdeveniments/evt-002              # 200, Noche de Monologos
curl -s http://localhost:3000/esdeveniments/evt-999              # 404 ESDEVENIMENT_NO_TROBAT
curl -s http://localhost:3000/esdeveniments/evt-003/sessions     # 200, total 2
curl -i -X POST http://localhost:3000/esdeveniments              # 405 amb Allow: GET, HEAD
curl -i http://localhost:3000/no-existeix                        # 404 RECURS_NO_TROBAT

  1. Per què a partir del Mòdul 6 farem servir Express

El nostre enrutador funciona, és curt i l'entens sencer. I tot i així, en un projecte real, gairebé ningú escriuria això. Val la pena ser honestos sobre el que no té:

Li falta Què implica
Middleware No hi ha manera de dir "això s'executa abans de totes les rutes" (registre, CORS, autenticació)
Subenrutadors Totes les rutes en una llista plana; res de muntar /api/v1 amb les seves rutes a dins
Ordre per especificitat Registrar en mal ordre fa inabastable una ruta, sense avís
Patrons rics Res d'opcionals, comodins ni restriccions per paràmetre
Anàlisi del cos, galetes, contingut negociat Cada cosa s'ha de fer a mà (comencem a 04-05)

Aleshores, per què hem fet això? Perquè Express no és màgia, és aquesta mateixa taula amb més anys a sobre. Quan a la lliçó 06-03 escriguis app.get('/esdeveniments/:id', gestor), sabràs que per dins hi ha un patró compilat a expressió regular, un objecte req.params que surt dels grups de captura i un despatxador que recorre una llista. I quan alguna cosa no funcioni —una ruta que no s'assoleix, un 404 inesperat, un gestor d'errors que no es dispara—, no estaràs depurant una caixa negra.

Errors Comuns i Consells

  • Casar contra req.url en comptes de url.pathname. Qualsevol paràmetre de consulta trenca la coincidència i produeix un 404 inexplicable.
  • Fer servir .+ en lloc de [^/]+ per als paràmetres. El patró es menja segments sencers i atropella altres rutes.
  • Oblidar les àncores ^ i $. /esdeveniments casaria dins de /admin/esdeveniments/esborrar.
  • Registrar /esdeveniments/:id abans que /esdeveniments/destacats. Guanya la primera que casa, i la literal queda inabastable.
  • Tornar 404 quan correspon 405. I si tornes 405, la capçalera Allow és obligatòria.
  • Passar la ruta a minúscules. Destrossa identificadors legítims com EV-2026-000123. Si vols tolerància, redirigeix amb 301.
  • Posar un try/catch a cada gestor. Repetició, incoherència i errors que s'empassen. Un de central i prou.
  • Consell: exposa enrutador.rutes i afegeix una ruta GET /rutes en desenvolupament que les llisti. És la documentació més barata que existeix.

Exercicis

Exercici 1: provar compilarPatro sense servidor

Escriu src/laboratori/provar-patrons.js que, sense aixecar cap servidor, comprovi aquests casos i imprimeixi una taula patro | ruta | casa | parametres: /esdeveniments contra /esdeveniments i /esdeveniments/evt-001; /esdeveniments/:id contra /esdeveniments/evt-001, /esdeveniments/evt-001/sessions i /esdeveniments/; /esdeveniments/:id/sessions/:sessioId contra /esdeveniments/evt-002/sessions/ses-002-1; i /informes/2026.json contra /informes/2026Xjson. Explica l'últim cas.

Exercici 2: DELETE i el 405

Registra DELETE /esdeveniments/:id a l'enrutador amb un gestor que llanci ESTAT_INVALID si l'esdeveniment té entrades venudes. Comprova amb curl -i que: DELETE /esdeveniments/evt-001 dóna 409, DELETE /esdeveniments/evt-999 dóna 404, PUT /esdeveniments/evt-001 dóna 405 amb Allow: DELETE, GET, HEAD, i DELETE /esdeveniments dóna 405 amb Allow: GET, HEAD.

Exercici 3: comptar rutes i temps

Afegeix a l'enrutador un comptador per ruta (fent servir registre.patro com a clau) amb el nombre de peticions i el temps total en mil·lisegons, i una ruta GET /metriques que el torni ordenat per temps mitjà descendent. Mesura'n l'efecte: quina ruta és la més lenta i per què?

Solucions

Solució 1. El cas interessant és l'últim: /informes/2026.json contra /informes/2026Xjson no casa, perquè el pas 1 de compilarPatro va escapar el punt i el va convertir en literal. Sense aquest escapament, . casaria amb qualsevol caràcter i la X s'hi colaria.

const { compilarPatro, aparellar } = require('../servidor/enrutador.js');

const casos = [
  ['/esdeveniments', '/esdeveniments'],
  ['/esdeveniments/:id', '/esdeveniments/evt-001'],
  ['/esdeveniments/:id', '/esdeveniments/evt-001/sessions'],   // null: [^/]+ no creua segments
  ['/esdeveniments/:id', '/esdeveniments/'],                   // null: + exigeix un caracter com a minim
  ['/informes/2026.json', '/informes/2026Xjson']               // null: el punt esta escapat
];

for (const [patro, ruta] of casos) {
  const parametres = aparellar(compilarPatro(patro), ruta);
  console.log(`${patro.padEnd(34)} | ${ruta.padEnd(34)} | ${parametres !== null} | ${JSON.stringify(parametres)}`);
}

Solució 2. El gestor només s'ocupa del domini; els estats els posa la taula de 04-02:

enrutador.registrar('DELETE', '/esdeveniments/:id', async (req, res, { parametres }) => {
  const esdeveniment = await cercarEsdevenimentObligatori(parametres.id);   // llanca 404
  if (esdeveniment.entradesVenudes > 0) {
    const error = new Error(`L'esdeveniment ${esdeveniment.id} te ${esdeveniment.entradesVenudes} entrades venudes`);
    error.codi = 'ESTAT_INVALID';                                           // -> 409
    throw error;
  }
  respondreSenseContingut(res, 204);
});

evt-001 té 276 entrades venudes, així que torna 409. Fixa't que el PUT dóna 405 amb DELETE i GET a Allow: l'enrutador ha recorregut la taula sencera acumulant els mètodes de tots els registres el patró dels quals casava, i per això l'Allow surt complet.

Solució 3. El comptador s'embolcalla al voltant de la crida al gestor, dins de despatxar:

const metriques = new Map();

async function invocarAmbMetrica(registre, req, res, context) {
  const inici = process.hrtime.bigint();
  try {
    return await registre.gestor(req, res, context);
  } finally {
    const ms = Number(process.hrtime.bigint() - inici) / 1e6;
    const previ = metriques.get(registre.patro) ?? { peticions: 0, msTotal: 0 };
    metriques.set(registre.patro, { peticions: previ.peticions + 1, msTotal: previ.msTotal + ms });
  }
}

El finally és imprescindible: sense ell, una petició que llança no es comptabilitzaria i les mitjanes mentirien justament en els casos que més interessen. La ruta més lenta serà /esdeveniments/:id/sessions, perquè obtenirEsdevenimentPerId recorre el catàleg després de llegir-lo; amb la memorització de la lliçó 03-01, la primera petició costa una lectura de disc i les següents gairebé res. Aquest salt entre el primer mesurament i els altres es veu perfectament a /metriques.

Conclusió

Escena Viva ja té una API navegable. Has vist que enrutar és casar (mètode, ruta) amb un gestor, que l'if/else sobre req.url està malament des del primer moment —compara la consulta juntament amb la ruta— i que es torna insostenible tan bon punt apareixen els paràmetres: índexs màgics, condicions de longitud i un ordre fràgil que ningú no gosa tocar.

L'alternativa és src/servidor/enrutador.js: una taula de { metode, patro, gestor } i un motor genèric. compilarPatro tradueix /esdeveniments/:id a /^\/esdeveniments\/([^\/]+)$/, i ara saps exactament per què cada peça és com és: les àncores ^ i $ perquè la ruta casi sencera, [^/]+ perquè un paràmetre no es mengi un segment veí, + en comptes de * per rebutjar el valor buit, i l'escapament previ perquè un punt del patró sigui un punt de veritat. aparellar extreu els grups de captura i els descodifica per segments, normalitzarRuta col·lapsa barres i treu la final, i les majúscules es respecten perquè les rutes les distingeixen. Les fallades també: 404 si no casa res, i 405 amb la capçalera Allow obligatòria si la ruta existeix amb un altre mètode.

I sobretot, tens el teu primer gestor d'errors centralitzat: un únic catch al despatxador que crida cosPerError, tradueix ESDEVENIMENT_NO_TROBAT a 404 i ESTAT_INVALID a 409, registra els 500 sense filtrar-los i comprova res.headersSent abans d'intentar respondre. Els gestors de src/servidor/rutes-api.js han quedat nets: fan la seva feina i llancen amb error.codi. Aquesta idea és la que Express formalitza a 06-07, i la reconeixeràs a l'instant perquè l'acabes d'escriure.

Amb GET /esdeveniments, GET /esdeveniments/:id, GET /esdeveniments/:id/sessions i GET /salut en marxa, l'API torna JSON. Però una plataforma de venda d'entrades també ha de mostrar una pàgina. A la lliçó següent, Servint Fitxers Estàtics, muntarem el mini front-end d'Escena Viva des d'aquest mateix servidor: mapejarem la URL a una ruta de disc protegida amb resoldreDinsDe —i veuràs en directe com GET /../../dades/esdeveniments.json s'emporta les teves dades sense aquesta defensa—, enviarem els fitxers amb createReadStream i pipeline en comptes de readFile, i aprendrem a respondre 304 Not Modified amb ETag perquè la segona visita no descarregui res.

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