A la lliçó anterior vam fer servir exactament dues funcions de fs: readFile i writeFile. Amb elles Escena Viva ja llegeix el seu catàleg del disc, i això va resoldre el deute que arrossegàvem des del Mòdul 1. Però un fitxer solt és el cas més senzill del sistema de fitxers.

Les aplicacions reals treballen amb arbres de directoris: cal saber si una cosa existeix i què és, quant ocupa i quan es va modificar; crear carpetes que encara no existeixen, llistar el que hi ha a dins, esborrar el que és vell, moure fitxers de lloc i, de vegades, llegir un tros concret d'un fitxer enorme sense carregar-lo sencer. Aquesta lliçó recorre aquesta segona meitat de fs i acaba construint una cosa que Escena Viva ja necessita: un magatzem d'informes que organitzi les sortides per mes, guardi l'informe d'ocupació del dia, llisti el que hi ha amb la seva mida i la seva data, i purgui automàticament el que passi de certa antiguitat.

Contingut

  1. stat i l'objecte Stats
  2. stat davant de lstat: enllaços simbòlics
  3. Recórrer directoris: readdir i withFileTypes
  4. Crear i esborrar arbres: mkdir, rm, rename, copyFile
  5. fs.constants, permisos i access
  6. Descriptors de fitxer i l'API FileHandle
  7. Vigilar canvis: watch davant de watchFile
  8. El magatzem d'informes d'Escena Viva

  1. stat i l'objecte Stats

stat respon a la pregunta «què sap el sistema operatiu sobre aquesta ruta?». Torna un objecte Stats amb les metadades del fitxer: no el seu contingut, sinó la seva fitxa.

const fs = require('node:fs/promises');

const info = await fs.stat('dades/esdeveniments.json');

console.log(info.isFile());        // true
console.log(info.isDirectory());   // false
console.log(info.size);            // 1893  (bytes)
console.log(info.mtime);           // 2026-08-11T09:14:22.417Z  (objecte Date)
Camp o mètode Tipus Què és
isFile() booleà És un fitxer regular
isDirectory() booleà És un directori
isSymbolicLink() booleà És un enllaç simbòlic (només té sentit amb lstat)
size número Mida en bytes. En un directori no és la suma del seu contingut
mtime / mtimeMs Date / número Última modificació del contingut. El que faràs servir gairebé sempre
ctime Date Últim canvi de metadades (permisos, propietari). No és «creació»
birthtime / atime Date Creació / últim accés. Cap dels dos no és fiable a tots els sistemes
mode número Permisos i tipus, en bits

Dues confusions clàssiques: ctime no significa creation time, sinó change time de les metadades —reanomenar o canviar permisos l'actualitza sense tocar el contingut—, i size en un directori no és el pes del que conté, sinó el del registre de directori mateix. Per saber quant ocupa una carpeta cal recórrer-la i sumar.

Com qualsevol operació de fs, stat falla amb ENOENT si la ruta no existeix. Això el converteix en la manera habitual —i correcta— de respondre a «això existeix i què és?» en una sola crida: embolcalla'l en un try/catch que torni null davant d'ENOENT i propagui qualsevol altre codi, tal com vam fer a la lliçó anterior amb llegirSiExisteix.

  1. stat davant de lstat: enllaços simbòlics

Un enllaç simbòlic és un fitxer que apunta a una altra ruta. La diferència entre les dues funcions és què fan en trobar-ne un:

Funció En topar amb un enllaç simbòlic
stat El segueix i torna les dades de la destinació
lstat No el segueix i torna les dades de l'enllaç mateix
const desti = await fs.stat('informes/ultim.json');       // segueix l'enllac
console.log(desti.isFile(), desti.isSymbolicLink());      // true  false

const enllac = await fs.lstat('informes/ultim.json');     // no el segueix
console.log(enllac.isFile(), enllac.isSymbolicLink());    // false true

La conseqüència pràctica importa: isSymbolicLink() sempre torna false amb stat, perquè aleshores ja has saltat a la destinació; i si l'enllaç apunta a una cosa esborrada —un enllaç trencat—, stat falla amb ENOENT mentre que lstat funciona i et descriu l'enllaç. Regla: fes servir stat quan t'interessi el contingut (el normal) i lstat quan estiguis recorrent o esborrant un arbre, per no seguir enllaços sense voler i acabar fora del directori que et pensaves que estaves processant.

  1. Recórrer directoris: readdir i withFileTypes

const noms = await fs.readdir('informes');
console.log(noms);
// [ '2026-07', '2026-08', 'vendes-per-sessio.json' ]

Per defecte readdir torna només noms, sense ruta i sense dir-te què és cada cosa. L'impuls natural és cridar stat sobre cadascun —for (const nom of await fs.readdir(dir)) { const info = await fs.stat(${dir}/${nom}); ... }—, i amb 5 entrades tant li fa; amb 50.000 són 50.000 crides al sistema extra.

L'opció withFileTypes ho resol: el sistema operatiu ja sap el tipus de cada entrada en llistar el directori, així que Node te'l torna de franc en forma d'objectes Dirent.

// EFICIENT: una sola crida, el tipus ve inclos.
const entrades = await fs.readdir('informes', { withFileTypes: true });

for (const entrada of entrades) {
  // entrada.name es el nom; entrada.parentPath, el directori que el conte.
  if (entrada.isDirectory()) console.log(`carpeta:  ${entrada.name}`);
  else if (entrada.isFile()) console.log(`fitxer:   ${entrada.name}`);
}

Un Dirent ofereix els mateixos predicats que Stats (isFile, isDirectory, isSymbolicLink) però no porta size ni mtime: si necessites mida o data, aleshores sí que toca stat. La regla és filtrar primer per tipus amb Dirent i fer stat només sobre el que t'interessa de debò. readdir accepta també recursive: true per recórrer l'arbre sencer d'una vegada, i no garanteix cap ordre: si necessites els informes ordenats per data, ordena'ls tu.

  1. Crear i esborrar arbres: mkdir, rm, rename, copyFile

// Crea l'arbre complet. Amb recursive, NO falla si ja existeix.
await fs.mkdir('informes/2026-08', { recursive: true });

// Esborra un arbre sencer. force evita l'ENOENT si ja no hi es.
await fs.rm('informes/2026-01', { recursive: true, force: true });

// Moure o reanomenar (atomic dins del mateix sistema de fitxers).
await fs.rename('informes/esborrany.json', 'informes/2026-08/ocupacio.json');

// Copiar sense sobreescriure la destinacio si ja existeix.
await fs.copyFile('dades/esdeveniments.json', 'dades/esdeveniments.copia.json', fs.constants.COPYFILE_EXCL);
Operació Opcions clau Comportament a recordar
mkdir recursive: true Crea els pares que falten i no falla amb EEXIST. Sense això, sí que falla
rm recursive, force recursive per a directoris amb contingut; force ignora que no existeixi
rename — Atòmic al mateix sistema de fitxers; falla amb EXDEV entre discs diferents
copyFile COPYFILE_EXCL Per defecte sobreescriu la destinació sense avisar
cp recursive: true Copia arbres sencers; l'equivalent de cp -r

Tres detalls que estalvien depuracions. Primer: mkdir amb recursive és idempotent, així que cridar-lo abans d'escriure és la manera neta de garantir la destinació, sense comprovacions prèvies ni condicions de cursa. Segon: rm sense recursive sobre un directori falla amb ERR_FS_EISDIR; i rm amb force i recursive no pregunta res, de manera que un rm(rutaCalculada, {recursive: true, force: true}) amb la ruta mal calculada esborra el que li posis al davant —valida sempre la ruta abans—. Tercer: rename falla amb EXDEV si origen i destinació són en discs o volums diferents; allà cal copiar i esborrar.

  1. fs.constants, permisos i access

fs.constants reuneix els valors numèrics que espera el sistema operatiu. Els d'ús diari són aquests:

Constant Per a què
F_OK Existeix la ruta?
R_OK / W_OK Puc llegir-la? Puc escriure-hi?
X_OK Puc executar-la (o entrar al directori)?
COPYFILE_EXCL copyFile falla si la destinació existeix
O_RDONLY, O_WRONLY, O_CREAT, O_APPEND Modes d'obertura de baix nivell per a open
const fs = require('node:fs/promises');
const { constants } = require('node:fs');

try {
  await fs.access('informes', constants.W_OK);
} catch {
  console.error('[informes] el directori no es escrivible per aquest proces');
}

access no torna res: compleix o llança. I aquí convé recordar la lliçó anterior: fer-lo servir com a pas previ a una operació reintrodueix la condició de cursa TOCTOU. El seu ús legítim és diagnòstic, a l'arrencada, per donar un missatge clar («no puc escriure a informes/») en comptes d'un EACCES críptic vint minuts després.

Els permisos es canvien amb chmod en notació octal: await fs.chmod(ruta, 0o600) deixa un fitxer llegible i escrivible només pel seu propietari, que és l'apropiat per a dades personals de compradors (hi tornarem al Mòdul 11). A Windows el model de permisos és diferent i chmod només afecta el bit de només lectura.

  1. Descriptors de fitxer i l'API FileHandle

readFile obre, llegeix sencer i tanca. Quan necessites més control —llegir només els primers bytes, escriure en una posició concreta, fer moltes operacions sobre el mateix fitxer sense reobrir-lo— treballes amb un descriptor de fitxer: un identificador que el sistema operatiu et lliura en obrir i que representa aquesta obertura concreta.

A node:fs/promises, open() torna un objecte FileHandle que embolcalla aquest descriptor:

// Llegeix nomes els primers bytes d'un fitxer, sense carregar-lo sencer.
async function llegirCapcalera(ruta, nombreDeBytes) {
  const gestor = await fs.open(ruta, 'r');

  try {
    const desti = Buffer.alloc(nombreDeBytes);
    // read(buffer, desplacamentEnBuffer, longitud, posicioEnFitxer)
    const { bytesRead } = await gestor.read(desti, 0, nombreDeBytes, 0);
    return desti.subarray(0, bytesRead);
  } finally {
    // SEMPRE. Encara que hi hagi error, encara que hi hagi return dins del try.
    await gestor.close();
  }
}

El try/finally no és opcional. Cada descriptor obert consumeix una entrada d'una taula del sistema operatiu amb un límit per procés (normalment uns quants milers); si una funció obre fitxers i no els tanca, cada crida perd un descriptor i, després d'unes hores de producció, el procés comença a fallar amb EMFILE: too many open files en operacions que no hi tenen res a veure. I fixa't on va el close: al finally, no al final del try, perquè allà qualsevol error del read s'enduria el tancament per davant. És el mateix raonament que aplicaràs a les connexions de base de dades al Mòdul 7.

Mètode de FileHandle Què fa
read(buffer, offset, length, position) Llegeix bytes en un Buffer des d'una posició
write(buffer | string, ...) Escriu en una posició concreta
readFile() / writeFile() / stat() Com les globals, però sobre el fitxer ja obert
truncate(n) Retalla el fitxer a n bytes
sync() Força l'abocament de la memòria cau del sistema al disc físic
close() Allibera el descriptor

sync() és la peça que completa l'escriptura atòmica de la lliçó anterior: escriure el temporal, sync() i només aleshores rename garanteix que el contingut és físicament al disc abans del canvi de nom.

  1. Vigilar canvis: watch davant de watchFile

Reaccionar a canvis en un fitxer sona trivial i no ho és. Node ofereix dos mecanismes de caràcter oposat:

fs.watch fs.watchFile
Com funciona Notificacions del sistema operatiu (inotify, FSEvents, ReadDirectoryChangesW) Sondeig: stat cada N mil·lisegons
Cost Molt baix Constant, encara que no canviï res
Latència Immediata Fins a un interval complet (5 s per defecte)
Fiabilitat Variable entre plataformes Predictible a tot arreu
Directoris recursius recursive: true (no a tots els sistemes) No

fs.watch té mala fama merescuda: pot emetre dos esdeveniments per un sol desament (molts editors escriuen un temporal i el reanomenen), el camp eventType ('rename' o 'change') no sempre significa el que sembla, el nom del fitxer arriba null en algunes plataformes, i en xarxes o volums muntats de vegades no arriba res.

const { watch } = require('node:fs');

// Recarrega del cataleg quan canvia la llavor, amb antirebot.
let temporitzador = null;

const vigilant = watch('dades/esdeveniments.json', (tipusEsdeveniment) => {
  clearTimeout(temporitzador);          // agrupa la rafega en una sola recarrega
  temporitzador = setTimeout(() => {
    console.error(`[cataleg] ${tipusEsdeveniment}: recarregant llavor`);
    obtenirCataleg({ recarregar: true }).catch((e) => console.error(e.message));
  }, 200);
});

process.on('SIGINT', () => vigilant.close());

El patró d'antirebot (debounce) amb setTimeout no és cap adorn: sense ell, un sol desament provoca dues o tres recàrregues. I observa el vigilant.close(): un vigilant manté el procés viu, així que cal tancar-lo perquè el programa pugui acabar. En projectes seriosos de vigilància, la recomanació honesta és fer servir una biblioteca com chokidar, que existeix precisament per normalitzar totes aquestes diferències.

  1. El magatzem d'informes d'Escena Viva

Ho ajuntem tot en un mòdul real. src/informes/ocupacio.js ja sap calcular resumirPerSala des del Mòdul 2; el que falta és on guardar aquests informes, com llistar-los i com evitar que creixin sense límit.

// src/informes/magatzem-informes.js
// Guarda, llista i purga els informes generats per Escena Viva.
// Estructura: informes/AAAA-MM/ocupacio-AAAA-MM-DD.json

const fs = require('node:fs/promises');

const DIRECTORI_BASE = 'informes';

// 2026-08-11T09:14:22.417Z -> { mes: '2026-08', dia: '2026-08-11' }
function particionarData(data = new Date()) {
  const dia = data.toISOString().slice(0, 10);
  return { mes: dia.slice(0, 7), dia };
}

// Guarda l'informe del dia. Si ja n'hi ha un, falla llevat que se sobreescrigui.
async function guardarInforme(contingut, { data = new Date(), sobreescriure = false } = {}) {
  const { mes, dia } = particionarData(data);
  const directori = `${DIRECTORI_BASE}/${mes}`;
  const ruta = `${directori}/ocupacio-${dia}.json`;

  // Idempotent: crea l'arbre si falta i no protesta si ja hi es.
  await fs.mkdir(directori, { recursive: true });

  try {
    await fs.writeFile(ruta, JSON.stringify(contingut, null, 2), {
      encoding: 'utf8',
      flag: sobreescriure ? 'w' : 'wx'   // wx: falla si ja existeix
    });
  } catch (error) {
    if (error.code === 'EEXIST') {
      const fallada = new Error(`Ja existeix l'informe de ${dia}. Fes servir sobreescriure per reemplacar-lo.`);
      fallada.codi = 'INFORME_DUPLICAT';
      throw fallada;
    }
    throw error;
  }

  return ruta;
}

// Noms de les carpetes mensuals existents. [] si no n'hi ha cap.
async function carpetesMensuals() {
  try {
    const entrades = await fs.readdir(DIRECTORI_BASE, { withFileTypes: true });
    // Filtrem per tipus amb Dirent: encara no hem fet ni un sol stat.
    return entrades.filter((e) => e.isDirectory()).map((e) => e.name).sort();
  } catch (error) {
    if (error.code === 'ENOENT') return [];   // encara no se n'ha generat cap
    throw error;
  }
}

// Llista tots els informes amb la seva mida i la seva data de modificacio.
async function llistarInformes() {
  const informes = [];

  for (const mes of await carpetesMensuals()) {
    const directori = `${DIRECTORI_BASE}/${mes}`;
    const fitxers = await fs.readdir(directori, { withFileTypes: true });

    for (const fitxer of fitxers) {
      if (!fitxer.isFile() || !fitxer.name.endsWith('.json')) continue;

      const ruta = `${directori}/${fitxer.name}`;
      const info = await fs.stat(ruta);   // stat nomes sobre el que ens interessa

      informes.push({
        mes,
        fitxer: fitxer.name,
        ruta,
        midaBytes: info.size,
        modificat: info.mtime.toISOString()
      });
    }
  }

  // readdir no garanteix ordre: l'imposem nosaltres.
  return informes.sort((a, b) => a.ruta.localeCompare(b.ruta));
}

// Esborra les carpetes mensuals anteriors als ultims N mesos.
async function purgarInformes(mesosAConservar = 6, { simular = true } = {}) {
  const limit = new Date();
  limit.setMonth(limit.getMonth() - mesosAConservar);
  const mesLimit = limit.toISOString().slice(0, 7);

  const purgades = [];

  for (const mes of await carpetesMensuals()) {
    // Comparar 'AAAA-MM' com a text funciona: el format es ordenable.
    if (mes >= mesLimit) continue;

    const ruta = `${DIRECTORI_BASE}/${mes}`;
    if (!simular) await fs.rm(ruta, { recursive: true, force: true });
    purgades.push(ruta);
  }

  return purgades;
}

module.exports = { guardarInforme, llistarInformes, purgarInformes, DIRECTORI_BASE };

Tres decisions de disseny mereixen comentari. El particionat per mes (informes/2026-08/) evita el problema clàssic de ficar cent mil fitxers en un sol directori, on qualsevol readdir es torna lent, i a més converteix la purga en un rm de carpeta sencera en comptes de mil esborrats solts. El flag: 'wx' per defecte fa que regenerar l'informe d'un dia ja guardat sigui un error explícit i no una sobreescriptura silenciosa: si de debò el vols, el demanes. I purgarInformes simula per defecte, perquè una funció que esborra recursivament ha d'exigir que li confirmis la intenció, no al revés. Fixa't també que les rutes es componen amb plantilles de text: funciona a Linux i macOS, però és exactament l'error que la lliçó següent corregirà amb path.join.

Errors Comuns i Consells

  • Cridar stat sobre cada entrada de readdir. Fes servir withFileTypes i reserva stat per quan necessitis size o mtime.
  • Interpretar ctime com a data de creació. És la data de l'últim canvi de metadades. Per a creació, birthtime —i només si el teu sistema la manté.
  • mkdir sense recursive falla amb EEXIST si la carpeta ja existeix i amb ENOENT si falta un pare. Amb recursive: true desapareixen tots dos problemes.
  • Deixar descriptors oberts. Tot open() necessita el seu close() en un finally. El símptoma tardà és EMFILE en un lloc sense relació amb la fallada.
  • copyFile sobreescriu per defecte (passa-li fs.constants.COPYFILE_EXCL si no ho vols), i readdir no garanteix cap ordre: ordena explícitament.
  • Consell: qualsevol funció que esborri ha de tenir mode simulació i fer-lo servir per defecte; rm amb recursive i force no demana confirmació a ningú. I si fs.watch et dispara callbacks duplicats, no busquis la fallada al teu codi: és el comportament normal, aplica antirebot.

Exercicis

Exercici 1: informe d'espai ocupat

Escriu src/laboratori/mesurar-carpeta.js que rebi una ruta per process.argv i recorri l'arbre recursivament calculant: nombre total de fitxers, nombre de directoris, mida total en bytes i els cinc fitxers més grans. Ha de fer servir readdir amb withFileTypes, no seguir enllaços simbòlics (lstat) i mostrar el resultat amb console.table per stdout.

Exercici 2: generar i purgar informes

Escriu src/informes/generar.js que faci servir obtenirCataleg() i resumirPerSala() per construir l'informe del dia, el guardi amb guardarInforme(), mostri el llistat complet amb llistarInformes() i executi purgarInformes(6) en mode simulació, indicant què s'esborraria. Accepta --sobreescriure i --purgar per passar de la simulació a l'acció.

Exercici 3: rotació d'un fitxer de registre

Escriu una funció rotarRegistre(ruta, midaMaximaBytes) que, si el fitxer supera la mida indicada, el reanomeni a ruta.AAAA-MM-DDTHH-mm-ss i deixi l'original buit, conservant com a màxim les cinc rotacions més recents i esborrant la resta. Ha de funcionar encara que el fitxer no existeixi encara.

Solucions

Solució 1. El recorregut recursiu amb withFileTypes és el patró que veuràs una vegada i una altra:

async function recorrer(ruta, acumulat) {
  const entrades = await fs.readdir(ruta, { withFileTypes: true });

  for (const entrada of entrades) {
    const fill = `${ruta}/${entrada.name}`;

    // isSymbolicLink() sobre un Dirent no segueix l'enllac: es el que volem.
    if (entrada.isSymbolicLink()) continue;

    if (entrada.isDirectory()) {
      acumulat.directoris += 1;
      await recorrer(fill, acumulat);
    } else if (entrada.isFile()) {
      const info = await fs.lstat(fill);
      acumulat.fitxers += 1;
      acumulat.bytes += info.size;
      acumulat.majors.push({ ruta: fill, bytes: info.size });
    }
  }

  return acumulat;
}

En acabar, majors.sort((a, b) => b.bytes - a.bytes).slice(0, 5) dona els cinc més grans. Saltar-se els enllaços simbòlics no és cap escrúpol teòric: un enllaç que apunti a un avantpassat converteix el recorregut en un bucle infinit.

Solució 2. El que importa és l'ordre de les operacions i que l'escriptura convisqui amb l'EEXIST:

const esdeveniments = await obtenirCataleg();
const resum = {
  generatEl: new Date().toISOString(),
  aforament: esdeveniments.reduce((t, e) => t + e.aforamentTotal, 0),
  venudes: esdeveniments.reduce((t, e) => t + e.entradesVenudes, 0),
  perSala: resumirPerSala(esdeveniments)
};

try {
  const ruta = await guardarInforme(resum, { sobreescriure });
  console.error(`[informes] guardat a ${ruta}`);
} catch (error) {
  if (error.codi !== 'INFORME_DUPLICAT') throw error;
  console.error(`[informes] ${error.message}`);
}

console.table(await llistarInformes());

const purgades = await purgarInformes(6, { simular: !purgar });
console.error(purgar
  ? `${purgades.length} carpetes esborrades.`
  : `S'esborrarien ${purgades.length} carpetes: ${purgades.join(', ')}`);

Solució 3. La rotació combina stat, rename i una purga per ordre invers:

async function rotarRegistre(ruta, midaMaximaBytes, rotacionsAConservar = 5) {
  let info;
  try {
    info = await fs.stat(ruta);
  } catch (error) {
    if (error.code === 'ENOENT') return false;   // res a rotar encara
    throw error;
  }

  if (info.size < midaMaximaBytes) return false;

  const marca = new Date().toISOString().replace(/[:.]/g, '-');
  await fs.rename(ruta, `${ruta}.${marca}`);
  await fs.writeFile(ruta, '', 'utf8');          // deixa l'original buit

  const directori = ruta.slice(0, ruta.lastIndexOf('/')) || '.';
  const base = ruta.slice(ruta.lastIndexOf('/') + 1);

  const rotacions = (await fs.readdir(directori, { withFileTypes: true }))
    .filter((e) => e.isFile() && e.name.startsWith(`${base}.`))
    .map((e) => e.name)
    .sort()
    .reverse();                                   // la marca ISO ordena per data

  for (const sobrant of rotacions.slice(rotacionsAConservar)) {
    await fs.rm(`${directori}/${sobrant}`, { force: true });
  }

  return true;
}

Dos detalls: els : i . de la marca ISO se substitueixen perquè no són vàlids en noms de fitxer a Windows, i ordenar alfabèticament les rotacions equival a ordenar-les per data perquè el format ISO està dissenyat per a això. El trossejat de la ruta amb lastIndexOf('/') és justament el que path.dirname i path.basename faran bé —i de manera portable— a la lliçó següent.

Conclusió

Ja coneixes la part de fs que va més enllà de llegir i escriure un fitxer. Saps consultar metadades amb stat i llegir un objecte Stats sense confondre ctime amb la data de creació; distingeixes stat de lstat i entens per què isSymbolicLink() només té sentit amb el segon; recorres directoris amb readdir i withFileTypes, evitant el stat per entrada que multiplica les crides al sistema; crees i destrueixes arbres amb mkdir({recursive}) —idempotent, i per això la manera neta de garantir una destinació— i amb rm({recursive, force}), que no pregunta res a ningú; mous amb rename sabent que falla amb EXDEV entre volums; i copies amb copyFile recordant que sobreescriu llevat de COPYFILE_EXCL.

Has vist a més els descriptors de fitxer i l'API FileHandle, amb la regla que no admet excepcions: tot open() porta el seu close() en un finally, o tard o d'hora arriba l'EMFILE. I coneixes els dos vigilants, watch i watchFile, amb els seus caràcters oposats i la necessitat d'antirebot. Escena Viva, per la seva banda, ha guanyat src/informes/magatzem-informes.js amb guardarInforme, llistarInformes i purgarInformes: els informes s'organitzen per mes, no se sobreescriuen per accident gràcies al flag: 'wx', es llisten amb la seva mida i la seva data, i es purguen en mode simulació llevat que confirmis el contrari.

Queda un cap solt, i és gros: totes les rutes d'aquest mòdul estan construïdes enganxant cadenes amb /. Funciona a la teva màquina i es trenca tan bon punt algú executi el projecte des d'un altre directori o a Windows. A Rutes Multiplataforma amb el Mòdul path veurem per què concatenar rutes és un error, la diferència exacta entre join i resolve, l'abisme entre process.cwd() i __dirname, com un ../../etc/passwd s'esmuny en una ruta construïda amb dades de l'usuari, i centralitzarem tota la configuració de rutes del projecte a src/config/rutes.js per refactoritzar el que hem escrit en aquestes dues lliçons.

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