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
staty el objetoStatsstatfrente alstat: enlaces simbólicos- Recorrer directorios:
readdirywithFileTypes - Crear y borrar árboles:
mkdir,rm,rename,copyFile fs.constants, permisos yaccess- Descriptores de fichero y la API
FileHandle - Vigilar cambios:
watchfrente awatchFile - El almacén de informes de Escena Viva
stat y el objeto Stats
stat y el objeto Statsstat 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.
stat frente a lstat: enlaces simbólicos
stat frente a lstat: enlaces simbólicosUn 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 trueLa 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.
- Recorrer directorios:
readdir y withFileTypes
readdir y withFileTypesconst 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ú.
- Crear y borrar árboles:
mkdir, rm, rename, copyFile
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.
fs.constants, permisos y access
fs.constants, permisos y accessfs.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.
- Descriptores de fichero y la API
FileHandle
FileHandlereadFile 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.
- Vigilar cambios:
watch frente a watchFile
watch frente a watchFileReaccionar 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.
- 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
statsobre cada entrada dereaddir. UsawithFileTypesy reservastatpara cuando necesitessizeomtime. - Interpretar
ctimecomo fecha de creación. Es la fecha del último cambio de metadatos. Para creación,birthtime—y solo si tu sistema la mantiene. mkdirsinrecursivefalla conEEXISTsi la carpeta ya existe y conENOENTsi falta un padre. Conrecursive: truedesaparecen los dos problemas.- Dejar descriptores abiertos. Todo
open()necesita suclose()en unfinally. El síntoma tardío esEMFILEen un sitio sin relación con el fallo. copyFilesobrescribe por defecto (pasafs.constants.COPYFILE_EXCLsi no lo quieres), yreaddirno garantiza ningún orden: ordena explícitamente.- Consejo: cualquier función que borre debe tener modo simulación y usarlo por defecto;
rmconrecursiveyforceno pide confirmación a nadie. Y sifs.watchte 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
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
