En la lección anterior usamos exactamente dos funciones de fs: readFile y writeFile. Con ellas Escena Viva ya lee su catálogo del disco, y eso resolvió la deuda que arrastrábamos desde el Módulo 1. Pero un fichero suelto es el caso más sencillo del sistema de archivos.

Las aplicaciones reales trabajan con árboles de directorios: hay que saber si algo existe y qué es, cuánto ocupa y cuándo se modificó; crear carpetas que aún no existen, listar lo que hay dentro, borrar lo viejo, mover ficheros de sitio y, a veces, leer un trozo concreto de un fichero enorme sin cargarlo entero. Esta lección recorre esa segunda mitad de fs y termina construyendo algo que Escena Viva necesita ya: un almacén de informes que organice las salidas por mes, guarde el informe de ocupación del día, liste lo que hay con su tamaño y su fecha, y purgue automáticamente lo que pase de cierta antigüedad.

Contenido

  1. stat y el objeto Stats
  2. stat frente a lstat: enlaces simbólicos
  3. Recorrer directorios: readdir y withFileTypes
  4. Crear y borrar árboles: mkdir, rm, rename, copyFile
  5. fs.constants, permisos y access
  6. Descriptores de fichero y la API FileHandle
  7. Vigilar cambios: watch frente a watchFile
  8. El almacén de informes de Escena Viva

  1. stat y el objeto Stats

stat responde a la pregunta "¿qué sabe el sistema operativo sobre esta ruta?". Devuelve un objeto Stats con los metadatos del fichero: no su contenido, sino su ficha.

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

const info = await fs.stat('datos/eventos.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  (objeto Date)
Campo o método Tipo Qué es
isFile() booleano Es un fichero regular
isDirectory() booleano Es un directorio
isSymbolicLink() booleano Es un enlace simbólico (solo tiene sentido con lstat)
size número Tamaño en bytes. En un directorio no es la suma de su contenido
mtime / mtimeMs Date / número Última modificación del contenido. El que usarás casi siempre
ctime Date Último cambio de metadatos (permisos, dueño). No es "creación"
birthtime / atime Date Creación / último acceso. Ninguno es fiable en todos los sistemas
mode número Permisos y tipo, en bits

Dos confusiones clásicas: ctime no significa creation time, sino change time de los metadatos —renombrar o cambiar permisos lo actualiza sin tocar el contenido—, y size en un directorio no es el peso de lo que contiene, sino el del propio registro de directorio. Para saber cuánto ocupa una carpeta hay que recorrerla y sumar.

Como cualquier operación de fs, stat falla con ENOENT si la ruta no existe. Eso lo convierte en la forma habitual —y correcta— de responder a "¿esto existe y qué es?" en una sola llamada: envuélvelo en un try/catch que devuelva null ante ENOENT y propague cualquier otro código, tal como hicimos en la lección anterior con leerSiExiste.

  1. stat frente a lstat: enlaces simbólicos

Un enlace simbólico es un fichero que apunta a otra ruta. La diferencia entre las dos funciones es qué hacen al encontrarse uno:

Función Al toparse con un enlace simbólico
stat Lo sigue y devuelve los datos del destino
lstat No lo sigue y devuelve los datos del enlace en sí
const destino = await fs.stat('informes/ultimo.json');    // sigue el enlace
console.log(destino.isFile(), destino.isSymbolicLink());  // true  false

const enlace = await fs.lstat('informes/ultimo.json');    // no lo sigue
console.log(enlace.isFile(), enlace.isSymbolicLink());    // false true

La consecuencia práctica importa: isSymbolicLink() siempre devuelve false con stat, porque para entonces ya has saltado al destino; y si el enlace apunta a algo borrado —un enlace roto—, stat falla con ENOENT mientras que lstat funciona y te describe el enlace. Regla: usa stat cuando te interese el contenido (lo normal) y lstat cuando estés recorriendo o borrando un árbol, para no seguir enlaces sin querer y acabar fuera del directorio que creías estar procesando.

  1. Recorrer directorios: readdir y withFileTypes

const nombres = await fs.readdir('informes');
console.log(nombres);
// [ '2026-07', '2026-08', 'ventas-por-sesion.json' ]

Por defecto readdir devuelve solo nombres, sin ruta y sin decirte qué es cada cosa. El impulso natural es llamar a stat sobre cada uno —for (const nombre of await fs.readdir(dir)) { const info = await fs.stat(${dir}/${nombre}); ... }—, y con 5 entradas da igual; con 50.000 son 50.000 llamadas al sistema extra.

La opción withFileTypes lo resuelve: el sistema operativo ya sabe el tipo de cada entrada al listar el directorio, así que Node te lo devuelve gratis en forma de objetos Dirent.

// EFICIENTE: una sola llamada, el tipo viene incluido.
const entradas = await fs.readdir('informes', { withFileTypes: true });

for (const entrada of entradas) {
  // entrada.name es el nombre; entrada.parentPath, el directorio que lo contiene.
  if (entrada.isDirectory()) console.log(`carpeta:  ${entrada.name}`);
  else if (entrada.isFile()) console.log(`fichero:  ${entrada.name}`);
}

Un Dirent ofrece los mismos predicados que Stats (isFile, isDirectory, isSymbolicLink) pero no trae size ni mtime: si necesitas tamaño o fecha, ahí sí toca stat. La regla es filtrar primero por tipo con Dirent y hacer stat solo sobre lo que te interesa de verdad. readdir acepta también recursive: true para recorrer el árbol entero de una vez, y no garantiza ningún orden: si necesitas los informes ordenados por fecha, ordénalos tú.

  1. Crear y borrar árboles: mkdir, rm, rename, copyFile

// Crea el arbol completo. Con recursive, NO falla si ya existe.
await fs.mkdir('informes/2026-08', { recursive: true });

// Borra un arbol entero. force evita el ENOENT si ya no esta.
await fs.rm('informes/2026-01', { recursive: true, force: true });

// Mover o renombrar (atomico dentro del mismo sistema de ficheros).
await fs.rename('informes/borrador.json', 'informes/2026-08/ocupacion.json');

// Copiar sin sobrescribir el destino si ya existe.
await fs.copyFile('datos/eventos.json', 'datos/eventos.respaldo.json', fs.constants.COPYFILE_EXCL);
Operación Opciones clave Comportamiento a recordar
mkdir recursive: true Crea los padres que falten y no falla con EEXIST. Sin ella, sí falla
rm recursive, force recursive para directorios con contenido; force ignora que no exista
rename — Atómico en el mismo sistema de ficheros; falla con EXDEV entre discos distintos
copyFile COPYFILE_EXCL Por defecto sobrescribe el destino sin avisar
cp recursive: true Copia árboles enteros; el equivalente de cp -r

Tres detalles que ahorran depuraciones. Primero: mkdir con recursive es idempotente, así que llamarlo antes de escribir es la forma limpia de garantizar el destino, sin comprobaciones previas ni condiciones de carrera. Segundo: rm sin recursive sobre un directorio falla con ERR_FS_EISDIR; y rm con force y recursive no pregunta nada, así que un rm(rutaCalculada, {recursive: true, force: true}) con la ruta mal calculada borra lo que le pongas delante —valida siempre la ruta antes—. Tercero: rename falla con EXDEV si origen y destino están en discos o volúmenes distintos; ahí hay que copiar y borrar.

  1. fs.constants, permisos y access

fs.constants reúne los valores numéricos que espera el sistema operativo. Los de uso diario son estos:

Constante Para qué
F_OK ¿Existe la ruta?
R_OK / W_OK ¿Puedo leerla? ¿Puedo escribirla?
X_OK ¿Puedo ejecutarla (o entrar en el directorio)?
COPYFILE_EXCL copyFile falla si el destino existe
O_RDONLY, O_WRONLY, O_CREAT, O_APPEND Modos de apertura de bajo nivel para 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 directorio no es escribible por este proceso');
}

access no devuelve nada: cumple o lanza. Y aquí conviene recordar la lección anterior: usarlo como paso previo a una operación reintroduce la condición de carrera TOCTOU. Su uso legítimo es diagnóstico, en el arranque, para dar un mensaje claro ("no puedo escribir en informes/") en lugar de un EACCES críptico veinte minutos después.

Los permisos se cambian con chmod en notación octal: await fs.chmod(ruta, 0o600) deja un fichero legible y escribible solo por su dueño, que es lo apropiado para datos personales de compradores (volveremos a ello en el Módulo 11). En Windows el modelo de permisos es distinto y chmod solo afecta al bit de solo lectura.

  1. Descriptores de fichero y la API FileHandle

readFile abre, lee entero y cierra. Cuando necesitas más control —leer solo los primeros bytes, escribir en una posición concreta, hacer muchas operaciones sobre el mismo fichero sin reabrirlo— trabajas con un descriptor de fichero: un identificador que el sistema operativo te entrega al abrir y que representa esa apertura concreta.

En node:fs/promises, open() devuelve un objeto FileHandle que envuelve ese descriptor:

// Lee solo los primeros bytes de un fichero, sin cargarlo entero.
async function leerCabecera(ruta, numeroDeBytes) {
  const manejador = await fs.open(ruta, 'r');

  try {
    const destino = Buffer.alloc(numeroDeBytes);
    // read(buffer, desplazamientoEnBuffer, longitud, posicionEnFichero)
    const { bytesRead } = await manejador.read(destino, 0, numeroDeBytes, 0);
    return destino.subarray(0, bytesRead);
  } finally {
    // SIEMPRE. Aunque haya error, aunque haya return dentro del try.
    await manejador.close();
  }
}

El try/finally no es opcional. Cada descriptor abierto consume una entrada de una tabla del sistema operativo con un límite por proceso (normalmente unos pocos miles); si una función abre ficheros y no los cierra, cada llamada filtra un descriptor y, tras unas horas de producción, el proceso empieza a fallar con EMFILE: too many open files en operaciones que no tienen nada que ver. Y fíjate en dónde va el close: en el finally, no al final del try, porque ahí cualquier error del read se llevaría por delante el cierre. Es el mismo razonamiento que aplicarás a las conexiones de base de datos en el Módulo 7.

Método de FileHandle Qué hace
read(buffer, offset, length, position) Lee bytes en un Buffer desde una posición
write(buffer | string, ...) Escribe en una posición concreta
readFile() / writeFile() / stat() Como las globales, pero sobre el fichero ya abierto
truncate(n) Recorta el fichero a n bytes
sync() Fuerza el volcado del caché del sistema al disco físico
close() Libera el descriptor

sync() es la pieza que completa la escritura atómica de la lección anterior: escribir el temporal, sync() y solo entonces rename garantiza que el contenido está físicamente en disco antes del cambio de nombre.

  1. Vigilar cambios: watch frente a watchFile

Reaccionar a cambios en un fichero suena trivial y no lo es. Node ofrece dos mecanismos de carácter opuesto:

fs.watch fs.watchFile
Cómo funciona Notificaciones del sistema operativo (inotify, FSEvents, ReadDirectoryChangesW) Sondeo: stat cada N milisegundos
Coste Muy bajo Constante, aunque no cambie nada
Latencia Inmediata Hasta un intervalo completo (5 s por defecto)
Fiabilidad Variable entre plataformas Predecible en todas partes
Directorios recursivos recursive: true (no en todos los sistemas) No

fs.watch tiene mala fama merecida: puede emitir dos eventos por un solo guardado (muchos editores escriben un temporal y renombran), el campo eventType ('rename' o 'change') no siempre significa lo que parece, el nombre del fichero llega null en algunas plataformas, y en redes o volúmenes montados a veces no llega nada.

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

// Recarga del catalogo cuando cambia la semilla, con antirrebote.
let temporizador = null;

const vigilante = watch('datos/eventos.json', (tipoEvento) => {
  clearTimeout(temporizador);           // agrupa la rafaga en una sola recarga
  temporizador = setTimeout(() => {
    console.error(`[catalogo] ${tipoEvento}: recargando semilla`);
    obtenerCatalogo({ recargar: true }).catch((e) => console.error(e.message));
  }, 200);
});

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

El patrón de antirrebote (debounce) con setTimeout no es un adorno: sin él, un solo guardado provoca dos o tres recargas. Y observa el vigilante.close(): un vigilante mantiene el proceso vivo, así que hay que cerrarlo para que el programa pueda terminar. En proyectos serios de vigilancia, la recomendación honesta es usar una biblioteca como chokidar, que existe precisamente para normalizar todas estas diferencias.

  1. El almacén de informes de Escena Viva

Juntamos todo en un módulo real. src/informes/ocupacion.js ya sabe calcular resumirPorSala desde el Módulo 2; lo que falta es dónde guardar esos informes, cómo listarlos y cómo evitar que crezcan sin límite.

// src/informes/almacen-informes.js
// Guarda, lista y purga los informes generados por Escena Viva.
// Estructura: informes/AAAA-MM/ocupacion-AAAA-MM-DD.json

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

const DIRECTORIO_BASE = 'informes';

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

// Guarda el informe del dia. Si ya existe uno, falla salvo que se sobrescriba.
async function guardarInforme(contenido, { fecha = new Date(), sobrescribir = false } = {}) {
  const { mes, dia } = particionarFecha(fecha);
  const directorio = `${DIRECTORIO_BASE}/${mes}`;
  const ruta = `${directorio}/ocupacion-${dia}.json`;

  // Idempotente: crea el arbol si falta y no protesta si ya esta.
  await fs.mkdir(directorio, { recursive: true });

  try {
    await fs.writeFile(ruta, JSON.stringify(contenido, null, 2), {
      encoding: 'utf8',
      flag: sobrescribir ? 'w' : 'wx'   // wx: falla si ya existe
    });
  } catch (error) {
    if (error.code === 'EEXIST') {
      const fallo = new Error(`Ya existe el informe de ${dia}. Usa sobrescribir para reemplazarlo.`);
      fallo.codigo = 'INFORME_DUPLICADO';
      throw fallo;
    }
    throw error;
  }

  return ruta;
}

// Nombres de las carpetas mensuales existentes. [] si no hay ninguna.
async function carpetasMensuales() {
  try {
    const entradas = await fs.readdir(DIRECTORIO_BASE, { withFileTypes: true });
    // Filtramos por tipo con Dirent: aun no hemos hecho ni un solo stat.
    return entradas.filter((e) => e.isDirectory()).map((e) => e.name).sort();
  } catch (error) {
    if (error.code === 'ENOENT') return [];   // aun no se ha generado ninguno
    throw error;
  }
}

// Lista todos los informes con su tamano y su fecha de modificacion.
async function listarInformes() {
  const informes = [];

  for (const mes of await carpetasMensuales()) {
    const directorio = `${DIRECTORIO_BASE}/${mes}`;
    const ficheros = await fs.readdir(directorio, { withFileTypes: true });

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

      const ruta = `${directorio}/${fichero.name}`;
      const info = await fs.stat(ruta);   // stat solo sobre lo que nos interesa

      informes.push({
        mes,
        fichero: fichero.name,
        ruta,
        tamanoBytes: info.size,
        modificado: info.mtime.toISOString()
      });
    }
  }

  // readdir no garantiza orden: lo imponemos nosotros.
  return informes.sort((a, b) => a.ruta.localeCompare(b.ruta));
}

// Borra las carpetas mensuales anteriores a los ultimos N meses.
async function purgarInformes(mesesAConservar = 6, { simular = true } = {}) {
  const limite = new Date();
  limite.setMonth(limite.getMonth() - mesesAConservar);
  const mesLimite = limite.toISOString().slice(0, 7);

  const purgadas = [];

  for (const mes of await carpetasMensuales()) {
    // Comparar 'AAAA-MM' como texto funciona: el formato es ordenable.
    if (mes >= mesLimite) continue;

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

  return purgadas;
}

module.exports = { guardarInforme, listarInformes, purgarInformes, DIRECTORIO_BASE };

Tres decisiones de diseño merecen comentario. El particionado por mes (informes/2026-08/) evita el problema clásico de meter cien mil ficheros en un solo directorio, donde cualquier readdir se vuelve lento, y además convierte la purga en un rm de carpeta entera en lugar de mil borrados sueltos. El flag: 'wx' por defecto hace que regenerar el informe de un día ya guardado sea un error explícito y no una sobrescritura silenciosa: si de verdad lo quieres, lo pides. Y purgarInformes simula por defecto, porque una función que borra recursivamente debe exigir que le confirmes la intención, no al revés. Fíjate también en que las rutas se componen con plantillas de texto: funciona en Linux y macOS, pero es exactamente el error que la lección siguiente corregirá con path.join.

Errores Comunes y Consejos

  • Llamar a stat sobre cada entrada de readdir. Usa withFileTypes y reserva stat para cuando necesites size o mtime.
  • Interpretar ctime como fecha de creación. Es la fecha del último cambio de metadatos. Para creación, birthtime —y solo si tu sistema la mantiene.
  • mkdir sin recursive falla con EEXIST si la carpeta ya existe y con ENOENT si falta un padre. Con recursive: true desaparecen los dos problemas.
  • Dejar descriptores abiertos. Todo open() necesita su close() en un finally. El síntoma tardío es EMFILE en un sitio sin relación con el fallo.
  • copyFile sobrescribe por defecto (pasa fs.constants.COPYFILE_EXCL si no lo quieres), y readdir no garantiza ningún orden: ordena explícitamente.
  • Consejo: cualquier función que borre debe tener modo simulación y usarlo por defecto; rm con recursive y force no pide confirmación a nadie. Y si fs.watch te dispara callbacks duplicados, no busques el fallo en tu código: es el comportamiento normal, aplica antirrebote.

Ejercicios

Ejercicio 1: informe de espacio ocupado

Escribe src/laboratorio/medir-carpeta.js que reciba una ruta por process.argv y recorra el árbol recursivamente calculando: número total de ficheros, número de directorios, tamaño total en bytes y los cinco ficheros más grandes. Debe usar readdir con withFileTypes, no seguir enlaces simbólicos (lstat) y mostrar el resultado con console.table por stdout.

Ejercicio 2: generar y purgar informes

Escribe src/informes/generar.js que use obtenerCatalogo() y resumirPorSala() para construir el informe del día, lo guarde con guardarInforme(), muestre el listado completo con listarInformes() y ejecute purgarInformes(6) en modo simulación, indicando qué se borraría. Acepta --sobrescribir y --purgar para pasar de la simulación a la acción.

Ejercicio 3: rotación de un fichero de registro

Escribe una función rotarRegistro(ruta, tamanoMaximoBytes) que, si el fichero supera el tamaño indicado, lo renombre a ruta.AAAA-MM-DDTHH-mm-ss y deje el original vacío, conservando como máximo las cinco rotaciones más recientes y borrando el resto. Debe funcionar aunque el fichero no exista todavía.

Soluciones

Solución 1. El recorrido recursivo con withFileTypes es el patrón que verás una y otra vez:

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

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

    // isSymbolicLink() sobre un Dirent no sigue el enlace: es lo que queremos.
    if (entrada.isSymbolicLink()) continue;

    if (entrada.isDirectory()) {
      acumulado.directorios += 1;
      await recorrer(hijo, acumulado);
    } else if (entrada.isFile()) {
      const info = await fs.lstat(hijo);
      acumulado.ficheros += 1;
      acumulado.bytes += info.size;
      acumulado.mayores.push({ ruta: hijo, bytes: info.size });
    }
  }

  return acumulado;
}

Al terminar, mayores.sort((a, b) => b.bytes - a.bytes).slice(0, 5) da los cinco mayores. Saltarse los enlaces simbólicos no es escrúpulo teórico: un enlace que apunte a un ancestro convierte el recorrido en un bucle infinito.

Solución 2. Lo importante es el orden de las operaciones y que la escritura conviva con el EEXIST:

const eventos = await obtenerCatalogo();
const resumen = {
  generadoEn: new Date().toISOString(),
  aforo: eventos.reduce((t, e) => t + e.aforoTotal, 0),
  vendidas: eventos.reduce((t, e) => t + e.entradasVendidas, 0),
  porSala: resumirPorSala(eventos)
};

try {
  const ruta = await guardarInforme(resumen, { sobrescribir });
  console.error(`[informes] guardado en ${ruta}`);
} catch (error) {
  if (error.codigo !== 'INFORME_DUPLICADO') throw error;
  console.error(`[informes] ${error.message}`);
}

console.table(await listarInformes());

const purgadas = await purgarInformes(6, { simular: !purgar });
console.error(purgar
  ? `${purgadas.length} carpetas borradas.`
  : `Se borrarian ${purgadas.length} carpetas: ${purgadas.join(', ')}`);

Solución 3. La rotación combina stat, rename y una purga por orden inverso:

async function rotarRegistro(ruta, tamanoMaximoBytes, rotacionesAConservar = 5) {
  let info;
  try {
    info = await fs.stat(ruta);
  } catch (error) {
    if (error.code === 'ENOENT') return false;   // nada que rotar todavia
    throw error;
  }

  if (info.size < tamanoMaximoBytes) return false;

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

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

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

  for (const sobrante of rotaciones.slice(rotacionesAConservar)) {
    await fs.rm(`${directorio}/${sobrante}`, { force: true });
  }

  return true;
}

Dos detalles: los : y . de la marca ISO se sustituyen porque no son válidos en nombres de fichero en Windows, y ordenar alfabéticamente las rotaciones equivale a ordenarlas por fecha porque el formato ISO está diseñado para eso. El troceado de la ruta con lastIndexOf('/') es justo lo que path.dirname y path.basename harán bien —y de forma portable— en la lección siguiente.

Conclusión

Ya conoces la parte de fs que va más allá de leer y escribir un fichero. Sabes consultar metadatos con stat y leer un objeto Stats sin confundir ctime con la fecha de creación; distingues stat de lstat y entiendes por qué isSymbolicLink() solo tiene sentido con el segundo; recorres directorios con readdir y withFileTypes, evitando el stat por entrada que multiplica las llamadas al sistema; creas y destruyes árboles con mkdir({recursive}) —idempotente, y por eso la forma limpia de garantizar un destino— y con rm({recursive, force}), que no pregunta nada a nadie; mueves con rename sabiendo que falla con EXDEV entre volúmenes; y copias con copyFile recordando que sobrescribe salvo COPYFILE_EXCL.

Has visto además los descriptores de fichero y la API FileHandle, con la regla que no admite excepciones: todo open() lleva su close() en un finally, o antes o después llega el EMFILE. Y conoces los dos vigilantes, watch y watchFile, con sus caracteres opuestos y la necesidad de antirrebote. Escena Viva, por su parte, ha ganado src/informes/almacen-informes.js con guardarInforme, listarInformes y purgarInformes: los informes se organizan por mes, no se sobrescriben por accidente gracias al flag: 'wx', se listan con su tamaño y su fecha, y se purgan en modo simulación salvo que confirmes lo contrario.

Queda un cabo suelto, y es grande: todas las rutas de este módulo están construidas pegando cadenas con /. Funciona en tu máquina y se rompe en cuanto alguien ejecute el proyecto desde otro directorio o en Windows. En Rutas Multiplataforma con el Módulo path veremos por qué concatenar rutas es un error, la diferencia exacta entre join y resolve, el abismo entre process.cwd() y __dirname, cómo un ../../etc/passwd se cuela en una ruta construida con datos del usuario, y centralizaremos toda la configuración de rutas del proyecto en src/config/rutas.js para refactorizar lo escrito en estas dos lecciones.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados