Skip to main content

Manejo del sistema de archivos con Node.js ES Modules

Node.js proporciona capacidades completas para interactuar con el sistema de archivos a través del módulo fs (file system). Este módulo permite realizar operaciones como lectura, escritura, modificación y eliminación de archivos y directorios.

Configuración Inicial para ES Modules

Para utilizar ES Modules en Node.js, es necesario configurar el proyecto adecuadamente:

package.json

{
"type": "module",
"name": "proyecto-fs",
"version": "1.0.0"
}

Importación del Módulo fs

El módulo fs ofrece dos APIs: la basada en callbacks y la basada en promesas. Para ES Modules, utilizaremos la versión de promesas:

// Importar la versión basada en promesas de fs
import fs from 'fs/promises';

📁 Ubicación del archivo genérica para un proyecto de backend

mi-backend-node/
├─ package.json
├─ README.md
├─ .env
├─ .gitignore
└─ src/
├─ app.mjs # Punto de entrada de la app (configura router, middlewares, etc.)
├─ server.mjs # Arranca el servidor HTTP nativo

├─ config/ # Configuración general
│ ├─ env.mjs # Carga variables de entorno (dotenv, etc.)
│ ├─ logger.mjs # Configuración de logs
│ └─ config.mjs # Config común (puertos, URLs, etc.)

├─ core/ # “Mini framework” propio (capa HTTP + router)
│ ├─ http-server.mjs # Crea y configura http.createServer
│ ├─ router.mjs # Router casero (registro de rutas y dispatch)
│ ├─ request.mjs # Helpers para parsear body, query, params
│ └─ response.mjs # Helpers para enviar respuestas JSON/HTML

├─ routes/ # Definición de rutas (capa de “vistas” de la API)
│ ├─ index.routes.mjs # Rutas base (healthcheck, etc.)
│ ├─ users.routes.mjs # Rutas /api/users
│ └─ auth.routes.mjs # Rutas /api/auth

├─ controllers/ # Controladores (C de MVC)
│ ├─ users.controller.mjs
│ └─ auth.controller.mjs

├─ models/ # Modelos de dominio y acceso a datos (M de MVC)
│ ├─ user.model.mjs # Lógica de datos de usuarios
│ └─ index.mjs # Punto central para exportar modelos

├─ repositories/ # (Opcional) Capa entre modelos y controladores
│ └─ user.repository.mjs

├─ services/ # Lógica de negocio reutilizable
│ ├─ auth.service.mjs
│ └─ user.service.mjs

├─ views/ # “Vistas”: plantillas o serializadores
│ ├─ templates/ # Si generas HTML (por ejemplo, emails o páginas simples)
│ │ └─ user-list.html
│ └─ serializers/ # Si tu vista son JSON “bonitos”
│ └─ user.serializer.mjs

├─ middlewares/ # Middlewares caseros (auth, logs, CORS, etc.)
│ ├─ cors.middleware.mjs
│ ├─ auth.middleware.mjs
│ └─ error-handler.mjs

├─ db/ # Conexión a base de datos / ficheros / JSON
│ ├─ connection.mjs # Conexión a SQLite, Postgres, etc. o manejo de fs
│ ├─ migrations/ # Scripts de migraciones (SQL, JSON, etc.)
│ └─ seeders/ # Datos iniciales

├─ utils/ # Utilidades genéricas
│ ├─ crypto.mjs # Hash de contraseñas, etc.
│ ├─ jwt.mjs # Helpers para firmar/verificar JWT
│ ├─ validate.mjs # Validaciones básicas
│ └─ date.mjs # Funciones de fecha, formateos, etc.

└─ tests/ # Tests
├─ unit/
│ ├─ users.controller.test.mjs
│ └─ auth.service.test.mjs
└─ integration/
└─ auth.routes.test.mjs
  • server.mjs: usa el módulo http de Node (http.createServer) y delega todo el manejo de peticiones a app.mjs.
  • app.mjs: “cableado” de la app. Carga configuración, crea el router, registra rutas, middlewares y pasa el handler final al servidor.
  • core/: aquí vive tu mini framework:
    • router.mjs define cómo registrar rutas (GET /api/users, POST /api/auth/login, etc.) y cómo encontrar el handler correcto para cada petición.
    • request.mjs y response.mjs encapsulan detalles feos de IncomingMessage y ServerResponse (parsear JSON, enviar status code, headers, etc.).
  • routes/: para cada recurso (users, auth, productos…) defines rutas y las conectas con los controladores. Aquí no hay lógica de negocio, solo “qué URL llama a qué controlador”.
  • controllers/: reciben la petición ya parseada (params, body, etc.), llaman a servicios/modelos y devuelven una respuesta adecuada. Son “traductores” entre HTTP y tu dominio.
  • models/: representan tus entidades y el acceso a datos (consultas SQL, lectura/escritura JSON, etc.).
  • repositories/ (opcional, pero muy cómodo): capa que encapsula la persistencia. El controlador llama al servicio, el servicio llama al repositorio, el repositorio llama al modelo o a la DB.
  • services/: lógica de negocio pura, sin preocuparse del HTTP. Ideal para reutilizar desde CLI, cron jobs, etc.
  • views/: en un backend tipo API puede ser:
    • plantillas HTML si generas páginas,
    • o “serializers” que definen cómo exponer tus modelos en JSON (qué campos mostrar, cómo formatear fechas, etc.).
  • middlewares/: funciones que se ejecutan antes o después de tu controlador (auth, CORS, logging, manejo centralizado de errores…).
  • db/: todo lo relacionado con la persistencia real (conexión, migraciones, seeders).
  • utils/: utilidades que no pertenecen a una capa específica.

En una arquitectura MVC modularizada, el lugar más común y más limpio para usar fs/promises es uno de estos tres, según el tipo de proyecto que tengas:

1. En la capa db/

Es el sitio más habitual cuando tu backend no usa una base de datos real y toda la persistencia depende de archivos JSON o textos.

Ejemplo típico:

src/
db/
json/
users.json
file-db.mjs ← Aquí usas fs/promises

file-db.mjs encapsula las operaciones de lectura/escritura:

import fs from "fs/promises";
import path from "path";

const filePath = path.join(process.cwd(), "src", "db", "json", "users.json");

export async function readUsers() {
const data = await fs.readFile(filePath, "utf8");
return JSON.parse(data);
}

export async function writeUsers(users) {
const json = JSON.stringify(users, null, 2);
await fs.writeFile(filePath, json);
}

Después, los modelos o los repositorios llaman aquí para obtener o guardar datos.

2. En repositories/

Si tu proyecto ya tiene más abstracción, fs/promises vive aquí.

Los repositorios son responsables de acceder a la persistencia, sea base de datos, archivo o API.

src/
repositories/
user.repository.mjs ← Aquí va fs/promises si lees/escribes JSON

Ejemplo:

import { readUsers, writeUsers } from "../db/file-db.mjs";

export class UserRepository {
async findAll() {
return await readUsers();
}

async create(user) {
const users = await readUsers();
users.push(user);
await writeUsers(users);
return user;
}
}

3. En models/ (cuando buscas simplicidad extrema)

Si no quieres repositorios ni una capa especial, el modelo puede llamar directamente a fs/promises.

Es válido, pero menos escalable.

src/
models/
user.model.mjs ← fs/promises vive aquí en proyectos pequeños

Ejemplo:

import fs from "fs/promises";
import path from "path";

const file = path.join(process.cwd(), "src", "data", "users.json");

export const UserModel = {
async all() {
const data = await fs.readFile(file, "utf8");
return JSON.parse(data);
}
};

Lugares donde no deberías usar fs/promises

Según este patrón arquitectónico:

  • controllers/

    Nunca. El controlador solo coordina, no accede a archivos.

  • routes/

    Nunca. Las rutas son solo “mapas” hacia controladores.

  • services/

    No es la opción ideal. El servicio debe tratar lógica de negocio, no persistencia.

  • middlewares/

    Tampoco, salvo casos muy específicos (por ejemplo, servir archivos estáticos manualmente).

  • views/

    No directamente. Si necesitas plantillas, que la carga la haga una capa inferior.

Cómo integrarlo por ejemplo, en un proyecto de backend:

Archivo: src/db/file-db.mjs

// Módulo que encapsula toda la lógica de lectura y escritura de archivos.
// Esta es la forma recomendada de usar fs/promises: aislado, modular y fácil de testear.

import fs from "fs/promises";
import path from "path";

// Construimos una ruta absoluta al archivo JSON de datos.
// process.cwd() devuelve el directorio desde donde se ejecuta "node".
// Esto evita errores cuando se ejecuta desde distintas carpetas.
const filePath = path.join(process.cwd(), "src", "db", "usuarios.json");

/**
* Función que lee el archivo usuarios.json y retorna su contenido como objeto JavaScript.
* Siempre devuelve un array u objeto típico del archivo JSON.
*/
export async function readUsers() {
try {
// Leemos el archivo como texto UTF-8.
const rawText = await fs.readFile(filePath, "utf8");

// Convertimos el texto JSON en objeto JS.
return JSON.parse(rawText);
} catch (error) {
// Si el archivo no existe o está corrupto, mostramos un error legible.
console.error("Error al leer usuarios.json:", error.message);

// Podemos retornar un array vacío como fallback, para evitar fallos en otras capas.
return [];
}
}

/**
* Función que sobrescribe el archivo usuarios.json con nuevos datos.
* Recibe un array u objeto, y lo convierte a JSON con formato legible.
*/
export async function writeUsers(usersData) {
try {
// Convertimos el objeto JS a JSON con sangría de 2 espacios.
const json = JSON.stringify(usersData, null, 2);

// Escribimos el archivo entero.
// Si el archivo no existe, Node lo crea automáticamente.
await fs.writeFile(filePath, json, "utf8");
} catch (error) {
console.error("Error al escribir usuarios.json:", error.message);
throw new Error("No se pudo guardar el archivo de usuarios");
}
}

/**
* Función auxiliar para añadir un usuario al archivo.
* Es un ejemplo muy básico de cómo construir una pequeña "API" interna de persistencia.
*/
export async function addUser(newUser) {
// 1. Leemos el estado actual del archivo.
const users = await readUsers();

// 2. Añadimos el nuevo usuario al array.
users.push(newUser);

// 3. Guardamos el archivo actualizado.
await writeUsers(users);

return newUser;
}

Archivo: src/models/user.model.mjs

// Ejemplo de modelo que usa el módulo de persistencia basado en fs/promises.
// Este modelo representa la "capa de dominio" del recurso Usuario,
// pero delega la persistencia al módulo file-db.mjs.

import { readUsers, addUser } from "../db/file-db.mjs";

export const UserModel = {
/**
* Devuelve todos los usuarios existentes.
* Aquí no se procesa nada más que la petición a la capa DB.
*/
async findAll() {
return await readUsers();
},

/**
* Crea un nuevo usuario dentro del archivo JSON usando la capa DB.
*/
async create({ nombre, email }) {
const user = {
id: crypto.randomUUID(),
nombre,
email
};

return await addUser(user);
}
};

Archivo: src/controllers/users.controller.mjs

// Controlador ejemplo que usa el modelo.
// Aquí no aparece fs/promises. Esa es la idea correcta.

import { UserModel } from "../models/user.model.mjs";

export async function listarUsuarios(req, res) {
const users = await UserModel.findAll();

res.json({
ok: true,
total: users.length,
data: users
});
}

export async function crearUsuario(req, res) {
const nuevo = await UserModel.create(req.body);

res.json({
ok: true,
message: "Usuario creado correctamente",
data: nuevo
});
}

Archivo de ejemplo src/db/usuarios.json

[]

Operaciones Básicas con Archivos

Leer un archivo de texto completo como string

// Ejemplo básico de lectura de un archivo de texto usando fs/promises.
// Este es el patrón más estándar cuando necesitas cargar contenido completo en memoria.

import fs from "fs/promises"; // Módulo nativo de Node.js para trabajar con Promesas
import path from "path";

// Obtenemos ruta absoluta al archivo.
// process.cwd() asegura que funcione incluso si ejecutamos el script desde otro lugar.
const filePath = path.join(process.cwd(), "archivos", "mensaje.txt");

async function leerArchivo() {
try {
// Leemos el archivo como texto con codificación UTF-8.
// Si no especificas "utf8", te dará un Buffer.
const contenido = await fs.readFile(filePath, "utf8");

console.log("Contenido del archivo:");
console.log(contenido);
} catch (error) {
// Cualquier error: archivo no existe, permisos, etc.
console.error("Error al leer el archivo:", error.message);
}
}

leerArchivo();

Leer archivo como Buffer y luego transformarlo

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos", "data.bin");

async function leerComoBuffer() {
try {
// Leemos como Buffer (sin codificación).
const buffer = await fs.readFile(ruta);

console.log("Buffer original:");
console.log(buffer);

// Convertimos a texto si sabemos que contiene UTF-8.
const texto = buffer.toString("utf8");

console.log("Buffer convertido a texto:");
console.log(texto);

} catch (error) {
console.error("Error:", error.message);
}
}

leerComoBuffer();

Leer línea a línea usando streaming manual (sin frameworks)

Node no tiene lectura línea a línea nativa como fs/promises, pero podemos combinar lectura completa + split.

import fs from "fs/promises";
import path from "path";

const filePath = path.join(process.cwd(), "archivos", "poema.txt");

async function leerLineas() {
try {
const contenido = await fs.readFile(filePath, "utf8");

// Convertimos cada salto de línea en un elemento del array.
const lineas = contenido.split("\n");

console.log("Líneas del archivo:");
lineas.forEach((linea, i) => {
console.log(`Línea ${i + 1}:`, linea.trim());
});

} catch (error) {
console.error("Error:", error.message);
}
}

leerLineas();

Leer un archivo dentro de try/catch y devolver fallback

Este patrón es útil cuando necesitas que el programa siga funcionando aunque el archivo falte.

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos", "config.txt");

async function leerConfig() {
try {
const texto = await fs.readFile(ruta, "utf8");
return texto;
} catch (error) {
console.warn("Archivo config no encontrado, usando valor por defecto.");
return "modo=produccion";
}
}

const config = await leerConfig();
console.log("Configuración final:", config);

Leer varios archivos en paralelo usando Promise.all

import fs from "fs/promises";
import path from "path";

const rutas = [
path.join(process.cwd(), "archivos", "a.txt"),
path.join(process.cwd(), "archivos", "b.txt"),
path.join(process.cwd(), "archivos", "c.txt")
];

async function leerMultiples() {
try {
// Promise.all lee los archivos en paralelo
const contenidos = await Promise.all(
rutas.map(r => fs.readFile(r, "utf8"))
);

console.log("Contenido de todos los archivos:");
contenidos.forEach((c, i) => {
console.log(`Archivo ${i + 1}:`);
console.log(c);
});

} catch (error) {
console.error("Error al leer uno de los archivos:", error.message);
}
}

leerMultiples();

Comprobar primero si el archivo existe (fs.access)

import fs from "fs/promises";
import path from "path";

const filePath = path.join(process.cwd(), "archivos", "notas.txt");

async function leerSiExiste() {
try {
// access permite comprobar si el archivo puede leerse.
await fs.access(filePath);

const contenido = await fs.readFile(filePath, "utf8");
console.log(contenido);

} catch (error) {
console.error("El archivo no existe o no puede leerse");
}
}

leerSiExiste();

Casos de uso más comunes

Leer un archivo de texto completo (readFile con UTF-8)

Caso de uso habitual: Cuando tu aplicación necesita cargar un archivo entero en memoria porque es pequeño y se usa como referencia.

Ejemplos reales:

  • Cargar un archivo config.txt o settings.txt.
  • Cargar una plantilla de email (template.html).
  • Leer un archivo changelog.txt, README.txt, etc.
  • Leer un pequeño diccionario, lista de palabras, frases, etc.

Es el uso más común: rápido, simple y directo.

Leer el archivo como Buffer (binario)

Caso de uso: Cuando necesitas trabajar con archivos que no son texto.

Ejemplos:

  • Leer una imagen (logo.png) para enviarla por HTTP.
  • Leer un PDF para adjuntarlo en una API.
  • Leer un archivo binario para procesarlo o transformarlo.
  • Almacenar archivos subidos por usuarios dentro de un directorio.

El Buffer es la representación cruda del archivo. Después puedes convertirlo a texto solo si sabes que contiene UTF-8.

Leer línea a línea (leyendo todo y haciendo split)

Caso de uso: Cuando necesitas procesar cada línea por separado, pero el archivo es pequeño y no requiere streaming real.

Situaciones típicas:

  • Leer un archivo de logs pequeño.
  • Leer un archivo CSV sencillo sin librerías externas.
  • Procesar un poema, listado, o configuraciones “una por línea”.
  • Cargar un archivo de tareas, comandos o palabras para un juego educativo.

Es útil en ejercicios o tareas educativas dentro de tus cursos.

Leer un archivo con fallback si no existe

Caso de uso: Cuando la aplicación debe seguir funcionando aunque el archivo falte.

Escenarios:

  • Si config.txt no existe, usar valores por defecto.
  • Si users.json no existe, devolver una lista vacía para que la API no rompa.
  • Si no existe la carpeta logs/, crearla o ignorarla dependiendo del caso.

Este patrón se usa mucho en sistemas que se autogestionan o auto-reparan.

Leer varios archivos en paralelo (Promise.all)

Caso de uso: Cuando necesitas cargar varios archivos al mismo tiempo, de forma rápida.

Ejemplos:

  • Cargar varios templates HTML (email bienvenida, email reset, email factura).
  • Leer varios archivos JSON de configuración.
  • Leer múltiples archivos de un proyecto estático.
  • Procesar varios textos al arrancar el servidor.

Es más rápido que leerlos uno por uno.

Comprobar si el archivo existe antes de leer (fs.access)

Caso de uso: Cuando quieres evitar errores al intentar leer un archivo que podría no existir.

Casos típicos:

  • Comprobar si existe un archivo de usuario antes de actualizarlo.
  • Comprobar si existe un archivo subido antes de eliminarlo.
  • Ver si existe un cache.txt para evitar regenerarlo.
  • Verificar si existe la carpeta uploads/ antes de guardarla.

Sirve para escribir código más seguro.

Resumen fácil

TécnicaCuándo usarla
Leer texto completoArchivos pequeños de configuración o plantillas
Leer como BufferArchivos binarios: imágenes, PDF, ZIP
Leer línea a líneaNecesitas procesar línea por línea (logs, listas)
Fallback si no existeTu app no debe romperse si falta el archivo
Leer varios en paraleloNecesitas rendimiento cargando múltiples archivos
Comprobar existenciaEvitar errores antes de leer/escribir

Escritura de Archivos

Sobrescribir o crear un archivo de texto (writeFile)

// Ejemplo 1: ESCRIBIR un archivo de texto desde cero,
// sobrescribiendo su contenido si ya existe.
// Uso típico: generar un archivo de configuración, un reporte, etc.

import fs from "fs/promises";
import path from "path";

// Construimos una ruta absoluta al archivo "saludo.txt" dentro de la carpeta "archivos".
const filePath = path.join(process.cwd(), "archivos", "saludo.txt");

async function escribirArchivoTexto() {
// Contenido que queremos guardar en el archivo.
const contenido = [
"Hola, este archivo ha sido generado por Node.js.",
"Puedes modificar este texto desde el código.",
"Cada vez que ejecutes el script, el archivo se sobrescribirá."
].join("\n"); // Unimos las líneas con saltos de línea

try {
// fs.writeFile crea el archivo si no existe,
// o lo sobrescribe completamente si ya existe.
await fs.writeFile(filePath, contenido, "utf8");

console.log("Archivo escrito correctamente en:", filePath);
} catch (error) {
// Si ocurre un error (permisos, ruta inválida, etc.), lo mostramos.
console.error("Error al escribir el archivo:", error.message);
}
}

// Ejecutamos la función.
escribirArchivoTexto();

Añadir contenido al final de un archivo (appendFile)

// Ejemplo 2: AÑADIR líneas al final de un archivo de texto existente.
// Uso típico: guardar logs, historiales de eventos, registros de acceso, etc.

import fs from "fs/promises";
import path from "path";

const logPath = path.join(process.cwd(), "logs", "app.log");

async function escribirLog(mensaje) {
// Preparamos una línea de log con marca de tiempo.
const linea = `[${new Date().toISOString()}] ${mensaje}\n`;

try {
// appendFile añade el texto al final del archivo indicado.
// Si el archivo NO existe, lo crea automáticamente.
await fs.appendFile(logPath, linea, "utf8");

console.log("Línea de log añadida a:", logPath);
} catch (error) {
console.error("Error al escribir en el log:", error.message);
}
}

// Ejemplo de uso:
async function main() {
await escribirLog("Servidor iniciado en puerto 3000");
await escribirLog("Petición GET /api/usuarios");
await escribirLog("Petición POST /api/login");
}

main();

Crear directorios si no existen y luego escribir

// Ejemplo 3: Asegurar que un directorio existe antes de escribir el archivo.
// Uso típico: sistema de subida de archivos, generación de informes en carpetas, backups, etc.

import fs from "fs/promises";
import path from "path";

async function guardarReporte(nombreArchivo, contenido) {
// Directorio donde guardaremos los reportes.
const reportsDir = path.join(process.cwd(), "reports");

// Ruta final del archivo dentro del directorio de reportes.
const filePath = path.join(reportsDir, nombreArchivo);

try {
// mkdir con { recursive: true } crea la carpeta si no existe.
// Si ya existe, no lanza error.
await fs.mkdir(reportsDir, { recursive: true });

// Escribimos el contenido del reporte.
await fs.writeFile(filePath, contenido, "utf8");

console.log("Reporte guardado en:", filePath);
} catch (error) {
console.error("Error al guardar el reporte:", error.message);
}
}

// Ejemplo de uso:
async function main() {
const contenidoReporte = [
"REPORTE DIARIO",
"==============",
"Usuarios activos: 23",
"Errores registrados: 0"
].join("\n");

await guardarReporte("reporte-2025-11-23.txt", contenidoReporte);
}

main();

Guardar datos JSON (mini “base de datos” con archivos)

// Ejemplo 4: Persistencia JSON simple.
// Uso típico: mini API sin BD, guardar usuarios, tareas, productos, etc.
// Este patrón es muy común en proyectos educativos o pequeños backends.

import fs from "fs/promises";
import path from "path";

const dbPath = path.join(process.cwd(), "data", "users.json");

/**
* Asegura que el archivo JSON existe.
* Si no existe, lo crea con un array vacío [].
*/
async function ensureJsonFile() {
try {
// Intentamos leer el archivo para ver si existe y es accesible.
await fs.access(dbPath);
} catch {
// Si falla, asumimos que no existe y lo creamos vacío.
console.warn("users.json no existe. Creando archivo nuevo...");
await fs.mkdir(path.dirname(dbPath), { recursive: true });
await fs.writeFile(dbPath, "[]", "utf8");
}
}

/**
* Lee el archivo users.json y devuelve el array de usuarios.
*/
async function readUsers() {
await ensureJsonFile();

const raw = await fs.readFile(dbPath, "utf8");
return JSON.parse(raw);
}

/**
* Guarda el array de usuarios sobrescribiendo el archivo.
*/
async function writeUsers(users) {
// JSON.stringify con null, 2 genera un JSON legible con sangrado.
const json = JSON.stringify(users, null, 2);
await fs.writeFile(dbPath, json, "utf8");
}

/**
* Añade un nuevo usuario al archivo.
*/
async function addUser(newUser) {
const users = await readUsers();
users.push(newUser);
await writeUsers(users);
return newUser;
}

// Ejemplo de uso:
async function main() {
const nuevoUsuario = {
id: Date.now(), // ID simple (mejor usar UUID en proyectos reales)
nombre: "Oscar",
email: "oscar@example.com"
};

const creado = await addUser(nuevoUsuario);
console.log("Usuario creado y guardado:", creado);

const todos = await readUsers();
console.log("Usuarios actuales en la 'BD' JSON:", todos);
}

main();

Crear un archivo de “backup” copiando datos

// Ejemplo 5: Crear una copia de seguridad (backup) de un archivo existente.
// Uso típico: antes de sobrescribir un archivo importante, guardas una copia.

import fs from "fs/promises";
import path from "path";

const originalPath = path.join(process.cwd(), "data", "users.json");
const backupPath = path.join(process.cwd(), "data", "backups", `users-backup-${Date.now()}.json`);

async function crearBackup() {
try {
// Leemos el contenido original.
const contenido = await fs.readFile(originalPath, "utf8");

// Nos aseguramos de que la carpeta de backups existe.
await fs.mkdir(path.dirname(backupPath), { recursive: true });

// Escribimos el contenido en un nuevo archivo backup.
await fs.writeFile(backupPath, contenido, "utf8");

console.log("Backup creado en:", backupPath);
} catch (error) {
console.error("No se pudo crear el backup:", error.message);
}
}

crearBackup();

Casos prácticos y usos comunes de estos patrones

1. Sobrescribir o crear un archivo de texto (writeFile simple)

Escenarios muy típicos:

  • Generar un archivo config.txt o config.json a partir de una interfaz de administración.
  • Crear reportes en texto plano: reporte-diario.txt, errores.txt, estadisticas.txt.
  • Exportar datos a un archivo para que luego alguien lo descargue (por ejemplo, una lista de usuarios en formato .txt).

En tu arquitectura, este tipo de código suele ir en:

  • services/ (si la lógica tiene que ver con negocio: generación de reportes),
  • o utils/ (si son funciones genéricas de exportación).

2. Añadir contenido a un archivo (appendFile)

Casos reales en backends educativos o sencillos:

  • Sistema de logs propio: cada petición se guarda como una línea en app.log.
  • Historial de accesos: guardar quién se conectó, desde dónde, cuándo.
  • Registro de errores funcionales o de negocio (por ejemplo, fallos de pago, intentos de login fallidos).

Este patrón es muy útil cuando aún no quieres montar un sistema de logging tipo Winston o similar, pero quieres algo más que console.log.

3. Crear directorios antes de escribir

Muy frecuente cuando empiezas a jugar con:

  • Subida de archivos (usuarios suben imágenes, documentos, etc.).

    Carpeta uploads/usuarios/123/avatar.png.

  • Generación de informes por fecha:

    reports/2025/11/reporte-2025-11-23.txt.

  • Sistemas de exportación de datos:

    exports/csv/usuarios-2025-11-23.csv.

El patrón mkdir({ recursive: true }) es casi obligatorio para evitar errores cuando el directorio aún no existe.

4. Guardar JSON como “mini base de datos”

Este es uno de los más importantes para tus cursos:

  • Simular una base de datos sin usar MySQL, PostgreSQL ni SQLite.
  • Guardar usuarios, tareas, productos, cursos, etc. en archivos .json.
  • Tener una API REST “real” que hace CRUD sobre un archivo JSON.
  • Muy útil como paso intermedio antes de introducir una base de datos.

En tu estructura MVC:

  • El código de lectura/escritura JSON suele vivir en db/ o repositories/.
  • Los models/ o services/ llaman a esas funciones para obtener o guardar datos.
  • Los controllers/ ni se enteran de que hay archivos, solo hablan con modelos/servicios.

5. Crear backups de archivos

Casos donde este patrón es muy práctico:

  • Antes de ejecutar una operación peligrosa (por ejemplo, borrar muchos registros del JSON), creas una copia en backups/.
  • Antes de migrar datos de un formato a otro.
  • Cuando quieres guardar el estado actual de la “BD” en un archivo con timestamp para poder restaurarlo manualmente.

Esto se suele usar en scripts de mantenimiento (scripts/), o en servicios de administración (services/admin.service.mjs).

Obtener Información Detallada de Archivos

Para obtener información detallada de un archivo en Node.js puro, se usa el método nativo:

fs.stat

o su versión basada en promesas:

fs.promises.stat

Este método devuelve un objeto Stats, que contiene toda la metadata del archivo.

Te lo explico paso a paso, con ejemplos muy claros, casos de uso y comentarios extendidos.

Obtener información detallada con fs.stat

import fs from "fs/promises";
import path from "path";

// Ruta absoluta del archivo que queremos inspeccionar.
const rutaArchivo = path.join(process.cwd(), "archivos", "info.txt");

async function obtenerInfoArchivo() {
try {
// fs.stat retorna un objeto Stats con TODA la información del archivo:
// tamaño, fechas de creación, tipo (archivo/directorio), permisos, etc.
const stats = await fs.stat(rutaArchivo);

console.log("Información detallada del archivo:");
console.log(stats);

} catch (error) {
console.error("Error al obtener información del archivo:", error.message);
}
}

obtenerInfoArchivo();

¿Qué contiene un objeto Stats?

El objeto devuelto por fs.stat contiene propiedades muy útiles:

stats.size                 → Tamaño del archivo en bytes
stats.isFile() → true si es un archivo normal
stats.isDirectory() → true si es un directorio
stats.isSymbolicLink() → true si es un enlace simbólico
stats.atime → Última vez que fue ACCEDIDO
stats.mtime → Última vez que fue MODIFICADO
stats.ctime → Última vez que cambió su "metadata"
stats.birthtime → Fecha de creación

Ejemplo práctico con impresión de propiedades clave

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos", "datos.txt");

async function infoDetallada() {
try {
const stats = await fs.stat(ruta);

console.log("¿Es archivo?", stats.isFile());
console.log("¿Es directorio?", stats.isDirectory());

console.log("Tamaño en bytes:", stats.size);
console.log("Fecha de creación:", stats.birthtime);
console.log("Última modificación:", stats.mtime);
console.log("Último acceso:", stats.atime);

} catch (error) {
console.error("No se pudo obtener la información:", error.message);
}
}

infoDetallada();

Validar si algo es archivo o carpeta

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos");

async function validarTipo() {
try {
const info = await fs.stat(ruta);

if (info.isDirectory()) {
console.log("La ruta apunta a un directorio.");
} else if (info.isFile()) {
console.log("La ruta apunta a un archivo.");
} else {
console.log("La ruta apunta a otro tipo de elemento.");
}

} catch (error) {
console.error("Ruta no válida:", error.message);
}
}

validarTipo();

Obtener tamaño de un archivo en KB o MB

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos", "video.mp4");

async function tamañoHumano() {
try {
const stats = await fs.stat(ruta);

const bytes = stats.size;
const kb = (bytes / 1024).toFixed(2);
const mb = (bytes / (1024 * 1024)).toFixed(2);

console.log(`Tamaño: ${bytes} bytes (${kb} KB, ${mb} MB)`);

} catch (error) {
console.error("No se pudo obtener tamaño:", error.message);
}
}

tamañoHumano();

CASOS DE USO COMUNES

1. Comprobar si una ruta es archivo, carpeta o enlace

Muy útil cuando procesas directorios, subidas de archivos, o estructuras dinámicas.

Ejemplo:

Antes de leer algo, quieres saber si realmente es un archivo.

2. Calcular tamaños de archivos

Por ejemplo:

  • verificar si un usuario sube un archivo demasiado grande;
  • monitorizar espacio usado por backups;
  • mostrar información en una API (“tamaño del archivo subido”).

3. Auditorías y logs

Puedes registrar:

  • cuándo se creó un archivo,
  • cuándo se modificó,
  • cuándo se accedió.

Esto sirve en sistemas de auditoría o trazabilidad.

4. Recorrer carpetas y filtrar tipos

Si quieres listar solo archivos .txt,

o solo carpetas dentro de otra carpeta,

usas fs.readdir + fs.stat.

5. Antes de realizar operaciones destructivas

Cuando vas a borrar algo, conviene saber si es:

  • un archivo real,
  • un directorio,
  • o algo inesperado.

6. Sistemas de almacenamiento propios

Muy común en APIs sin framework que guardan:

  • documentos,
  • imágenes,
  • datos JSON.

Sabes exactamente qué tipo de elemento estás manejando.

7. Recuperar datos para dashboards o paneles

Ejemplo:

  • mostrar la fecha de último backup,
  • tamaño del archivo con registros,
  • número total de archivos en una carpeta.

Operaciones con Directorios

Crear un directorio (fs.mkdir)

// EJEMPLO 1: Crear un directorio usando fs.mkdir
// Este patrón es fundamental cuando tu aplicación necesita generar carpetas
// para guardar información, como "uploads", "logs", "reportes", etc.

import fs from "fs/promises";
import path from "path";

// Construimos una ruta absoluta a la carpeta que queremos crear.
// Usamos process.cwd() para que funcione independientemente del punto
// desde el que se ejecute "node".
const ruta = path.join(process.cwd(), "reportes", "2025");

async function crearDirectorio() {
try {
// fs.mkdir crea un directorio. Si usamos la opción `{ recursive: true }`,
// Node creará todos los subdirectorios necesarios sin provocar errores.
// Si la carpeta ya existe, no falla, simplemente lo ignora.
await fs.mkdir(ruta, { recursive: true });

console.log("Directorio creado en:", ruta);
} catch (error) {
// Si ocurre cualquier error (permisos, rutas inválidas, etc.),
// lo capturamos aquí.
console.error("Error al crear el directorio:", error.message);
}
}

crearDirectorio();

Comprobar si un directorio existe (fs.access)

// EJEMPLO 2: Comprobar si un directorio existe antes de usarlo.
// Este patrón se utiliza muchísimo para evitar errores al intentar leer
// o escribir dentro de un directorio que podría no existir.

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "uploads");

async function existeDirectorio() {
try {
// Intentamos acceder al directorio.
// Si existe y es accesible, no lanzará error.
await fs.access(ruta);

console.log("El directorio existe.");
} catch {
// Si el directorio NO existe o no es accesible por permisos,
// caemos al catch.
console.log("El directorio NO existe.");
}
}

existeDirectorio();

Leer el contenido de un directorio (fs.readdir)

// EJEMPLO 3: Leer el contenido de un directorio.
// fs.readdir devuelve un array con los nombres de los elementos (archivos y carpetas)
// que contiene la ruta indicada.

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos");

async function leerDirectorio() {
try {
// Obtenemos la lista de elementos dentro del directorio.
// El resultado es un array de strings, cada uno representando
// el nombre de un archivo o subdirectorio.
const elementos = await fs.readdir(ruta);

console.log("Contenido del directorio:");
console.log(elementos);
} catch (error) {
console.error("Error al leer el directorio:", error.message);
}
}

leerDirectorio();

Leer un directorio y detectar archivos o subdirectorios

// EJEMPLO 4: Leer un directorio con información adicional sobre cada elemento.
// Usamos la opción { withFileTypes: true } para obtener objetos Dirent,
// que permiten saber si un elemento es archivo o carpeta.

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "archivos");

async function leerConTipos() {
try {
// Con withFileTypes obtenemos objetos especiales que contienen
// métodos como isFile(), isDirectory(), isSymbolicLink(), etc.
const items = await fs.readdir(ruta, { withFileTypes: true });

for (const item of items) {
if (item.isFile()) {
console.log("Archivo encontrado:", item.name);
} else if (item.isDirectory()) {
console.log("Carpeta encontrada:", item.name);
} else {
console.log("Elemento desconocido:", item.name);
}
}

} catch (error) {
console.error("Error al leer con tipos:", error.message);
}
}

leerConTipos();

Eliminar un directorio (fs.rm)

// EJEMPLO 5: Eliminar un directorio por completo.
// fs.rm con { recursive: true } permite borrar directorios con contenido.
// Esta acción es destructiva, pero necesaria en procesos de limpieza o reseteo.

import fs from "fs/promises";
import path from "path";

const ruta = path.join(process.cwd(), "temp");

async function borrarDirectorio() {
try {
// El modo recursive elimina todos los archivos y subcarpetas dentro.
// El modo force ignora errores típicos como "no existe".
await fs.rm(ruta, { recursive: true, force: true });

console.log("Directorio eliminado correctamente:", ruta);
} catch (error) {
console.error("Error al eliminar el directorio:", error.message);
}
}

borrarDirectorio();

Recorrer un directorio COMPLETO de forma recursiva

// EJEMPLO 6: Recorrer una estructura completa de carpetas y archivos.
// Este patrón se utiliza para:
// - construir un explorador de archivos,
// - procesar todos los elementos dentro de un proyecto,
// - buscar archivos por extensión,
// - generar listados completos de contenido.

import fs from "fs/promises";
import path from "path";

async function listarRecursivo(ruta) {
try {
// Con withFileTypes podemos distinguir archivos y carpetas fácilmente.
const items = await fs.readdir(ruta, { withFileTypes: true });

for (const item of items) {
// Obtenemos la ruta absoluta de cada elemento encontrado.
const rutaCompleta = path.join(ruta, item.name);

if (item.isDirectory()) {
// Si es carpeta, la mostramos y volvemos a llamar recursivamente
// a esta misma función para seguir explorando dentro.
console.log("Carpeta:", rutaCompleta);
await listarRecursivo(rutaCompleta);
} else {
// Si es archivo, simplemente lo mostramos.
console.log("Archivo:", rutaCompleta);
}
}
} catch (error) {
console.error("Error al recorrer directorio:", error.message);
}
}

// Ruta inicial desde donde quieres empezar a explorar.
const rutaInicial = path.join(process.cwd(), "archivos");

// Iniciamos la exploración recursiva.
listarRecursivo(rutaInicial);

Mover o renombrar un directorio (fs.rename)

// EJEMPLO 7: Renombrar o mover un directorio a otra ubicación.
// fs.rename sirve tanto para cambiar el nombre como para mover carpetas
// dentro del sistema de archivos.

import fs from "fs/promises";
import path from "path";

// Directorio original
const origen = path.join(process.cwd(), "datos");

// Nueva ubicación o nuevo nombre
const destino = path.join(process.cwd(), "datos_backup");

async function moverDirectorio() {
try {
// Node moverá el directorio completo a la nueva ruta.
await fs.rename(origen, destino);

console.log("Directorio movido o renombrado correctamente.");
} catch (error) {
console.error("Error al mover o renombrar:", error.message);
}
}

moverDirectorio();

Casos de uso típicos y sencillos en backends

1. Subida de archivos

Crear carpetas dinámicamente:

  • /uploads
  • /uploads/usuarios/123/
  • /uploads/productos/2025/

2. Logs del servidor

Carpetas como:

  • /logs
  • /logs/2025/
  • /logs/errors/

3. Generación de reportes

Guardar archivos automáticamente:

  • /reportes/2025/mes-11/reporte.txt
  • /estadisticas/usuarios/

4. Limpieza automatizada

Eliminar:

  • carpetas temporales,
  • carpetas de cache,
  • carpetas antiguas de backups.

5. Construir utilidades internas

Un explorador recursivo de directorios para:

  • listar plantillas,
  • listar recursos,
  • buscar archivos por extensión.

6. Reorganización automatizada

Mover carpetas completas:

  • de /data/actual/ a /data/backup/,
  • renombrar carpetas según fechas.

7. Sistemas educativos o ejercicios

Crear carpetas de forma dinámica para cada alumno:

  • /alumnos/juan-proyecto/,
  • /ejercicios/actividad-1/.

Manejo de Archivos Grandes con Streams

Ahora vamos a entrar en cómo manejar archivos grandes usando streams en Node.js puro.

La idea principal es esta: Cuando un archivo es demasiado grande, no debes leerlo entero en memoria (readFile), sino procesarlo en partes usando streams.

Esto permite trabajar con archivos de cientos de MB o varios GB sin bloquear el servidor.

Concepto básico: Readable Streams (lectura por partes)

Un stream de lectura permite leer un archivo poco a poco, en bloques llamados chunks.

// EJEMPLO 1: Leer un archivo grande usando un Readable Stream.
//
// Este patrón permite procesar el archivo en trozos (chunks) sin cargarlo entero en memoria.
//
// MUY útil cuando:
// - el archivo es muy grande (videos, logs enormes, backups, etc.)
// - necesitas procesar datos a medida que llegan
// - quieres evitar bloqueos de memoria o caídas del servidor

import fs from "fs";
import path from "path";

const ruta = path.join(process.cwd(), "archivos_grandes", "video.mp4");

// Creamos un stream de lectura.
// fs.createReadStream lee el archivo parte por parte.
const stream = fs.createReadStream(ruta, {
highWaterMark: 64 * 1024 // Tamaño del chunk: 64KB por lectura (valor recomendado)
});

// Evento 'data': se ejecuta cada vez que llega un chunk.
stream.on("data", chunk => {
console.log("Chunk recibido:", chunk.length, "bytes");
// Aquí podrías procesarlo, enviarlo por HTTP, transformarlo, etc.
});

// Evento 'end': cuando el archivo ha sido leído completamente.
stream.on("end", () => {
console.log("Lectura completa del archivo.");
});

// Evento 'error': si ocurre un problema (archivo no encontrado, permisos, etc.)
stream.on("error", error => {
console.error("Error al leer archivo grande:", error.message);
});

Escribir archivos grandes con Write Streams

Cuando necesitas escribir muchos datos en un archivo enorme, un writeFile sería lento o consumiría demasiada memoria.

En su lugar, usas fs.createWriteStream().

// EJEMPLO 2: Escribir un archivo grande usando WriteStream
//
// Este patrón evita cargar todo el contenido en memoria antes de escribir.
// En su lugar, vamos escribiendo chunk a chunk.

import fs from "fs";
import path from "path";

const destino = path.join(process.cwd(), "salidas", "salida_grande.txt");

// Creamos un stream de escritura.
const stream = fs.createWriteStream(destino, {
flags: "a", // "a" añade al final; "w" sobrescribe
encoding: "utf8"
});

// Simulamos escritura de muchos datos.
async function escribirMuchoTexto() {
for (let i = 1; i <= 50000; i++) {
const linea = `Línea número ${i}\n`;

// write devuelve false si el buffer interno está lleno.
const puedeEscribir = stream.write(linea);

// Si el buffer se ha llenado, esperamos al evento "drain".
if (!puedeEscribir) {
await new Promise(resolve => stream.once("drain", resolve));
}
}

// Cerramos el stream correctamente.
stream.end(() => {
console.log("Archivo grande generado correctamente.");
});
}

escribirMuchoTexto();

Comentarios importantes:

  • El método .write() puede saturar el buffer interno.
  • Si devuelve false, debes esperar al evento "drain" antes de seguir escribiendo.
  • Esto asegura que no saturas la memoria del servidor.

Copiar archivos grandes usando Streams

Este patrón es más eficiente que fs.copyFile cuando el archivo es enorme.

// EJEMPLO 3: Copiar un archivo grande mediante streaming.
//
// Este método no carga el archivo en memoria; lo va copiando por chunks.
// Ideal para copiar vídeos, backups grandes o archivos ZIP pesados.

import fs from "fs";
import path from "path";

const origen = path.join(process.cwd(), "archivos_grandes", "video.mp4");
const destino = path.join(process.cwd(), "copias", "video_copia.mp4");

// Creamos stream de lectura y escritura
const leer = fs.createReadStream(origen);
const escribir = fs.createWriteStream(destino);

// Conectamos ambos streams usando pipe:
leer.pipe(escribir);

// Eventos para control
leer.on("error", e => console.error("Error al leer:", e.message));
escribir.on("error", e => console.error("Error al escribir:", e.message));

escribir.on("finish", () => {
console.log("Archivo copiado correctamente mediante streaming.");
});

El método .pipe() es uno de los más poderosos de Node, porque:

  • conecta la salida del stream de lectura
  • con la entrada del stream de escritura
  • gestionando automáticamente los eventos "drain" y la presión de memoria (backpressure)

Servir un archivo grande por HTTP sin consumir memoria

Este es uno de los usos más comunes en un backend real:

// EJEMPLO 4: Servir un archivo grande desde un servidor HTTP nativo.
// Esto puede ser un video, PDF, imagen enorme o cualquier archivo pesado.
//
// Node enviará el archivo en chunks según se vaya leyendo.

import http from "http";
import fs from "fs";
import path from "path";

const rutaVideo = path.join(process.cwd(), "archivos_grandes", "video.mp4");

const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "video/mp4" });

// Creamos stream de lectura
const stream = fs.createReadStream(rutaVideo);

// Enviamos al cliente chunk a chunk sin cargar todo el video en RAM
stream.pipe(res);

// Manejo de errores
stream.on("error", error => {
res.writeHead(500);
res.end("Error interno al leer el archivo.");
});
});

server.listen(3000, () => {
console.log("Servidor listo en http://localhost:3000");
});

Este ejemplo es una base perfecta para:

  • streaming de videos,
  • servir imágenes,
  • servir PDFs grandes,
  • servir cualquier archivo pesado.

Leer un archivo línea por línea usando Streams (ideal para logs grandes)

// EJEMPLO 5: Procesar un archivo enorme línea por línea.
//
// Este patrón es muy útil para leer logs gigantes,
// sin reventar la memoria cargando todo el archivo.

import fs from "fs";
import readline from "readline";
import path from "path";

const ruta = path.join(process.cwd(), "logs", "log_grande.txt");

// Creamos un stream de lectura
const stream = fs.createReadStream(ruta);

// readline crea una interfaz que lee línea por línea desde el stream
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});

async function leerLineas() {
let contador = 0;

// El for await permite leer cada línea individualmente
for await (const linea of rl) {
contador++;
console.log("Línea", contador, "→", linea);

// Aquí podrías filtrar, procesar, guardar en BD, etc.
}

console.log("Archivo procesado completamente.");
}

leerLineas();

Esto es especialmente útil cuando el archivo:

  • supera los 100 MB,
  • contiene millones de líneas,
  • no puedes cargarlo entero en RAM.

CASOS DE USO REALES

1. Manejar archivos enormes sin bloquear el servidor

Videos, imágenes, PDFs, SQL dumps, logs enormes, backups.

2. Servir contenido multimedia

Streaming de audio o video, descarga de PDFs grandes, etc.

3. Procesar logs enormes línea a línea

Ideal para sistemas de monitorización o análisis.

4. Generar archivos con muchos datos

Por ejemplo:

  • exportación CSV gigante,
  • generación masiva de registros,
  • informes pesados.

5. Copiar archivos de forma eficiente

Sin saturar la RAM del servidor.

6. Cargar datos gradualmente

Útil al trabajar con datasets de aprendizaje o archivos NDJSON.

Detectar cambios en tiempo real

Detectar cambios en tiempo real con fs.watch

fs.watch sirve para vigilar archivos o directorios y ejecutar código cada vez que hay un cambio:

  • cuando un archivo se crea,
  • cuando se modifica,
  • cuando se borra,
  • cuando cambia un directorio.

Es útil para automatizar tareas, recargar proyectos, monitorizar logs, etc.

Ejemplo 1: Vigilar cambios en un archivo específico

// EJEMPLO 1: Vigilar un archivo concreto.
// fs.watch recibe DOS cosas:
// 1. la ruta a vigilar
// 2. un callback que se ejecuta cada vez que detecta un cambio
//
// Esta técnica sirve para recargar configuraciones, observar logs, etc.

import fs from "fs";
import path from "path";

const archivo = path.join(process.cwd(), "config", "settings.json");

// Activamos vigilancia sobre el archivo
fs.watch(archivo, (evento, nombreArchivo) => {
// "evento" suele ser "change" cuando el archivo se modifica
// "nombreArchivo" es el nombre concreto que ha cambiado
console.log("Cambio detectado:", evento, "en", nombreArchivo);

// Lógica típica: recargar configuración o volver a leer el archivo
});

Ejemplo 2: Vigilar un directorio entero

// EJEMPLO 2: Vigilar un directorio completo.
//
// Aquí el watcher se activa cuando:
// - se crea un archivo nuevo
// - se modifica uno existente
// - se borra alguno
//
// Muy útil para subidas de archivos, sistemas de logs, exportaciones, etc.

import fs from "fs";
import path from "path";

const carpeta = path.join(process.cwd(), "uploads");

fs.watch(carpeta, (evento, archivo) => {
// Puede que "archivo" llegue como undefined en algunos sistemas.
// Por eso conviene comprobarlo.
console.log("Cambio detectado:", evento);

if (archivo) {
console.log("Elemento afectado:", archivo);
}
});

Ejemplo 3: Un watcher práctico para logs en tiempo real

// EJEMPLO 3: Ver cambios en un log en tiempo real.
// Funciona como un "tail -f" pero en Node.

import fs from "fs";
import path from "path";

const rutaLog = path.join(process.cwd(), "logs", "app.log");

fs.watch(rutaLog, () => {
// Leemos solo cuando cambia
const contenido = fs.readFileSync(rutaLog, "utf8");
console.log("Nuevo contenido del log:");
console.log(contenido);
});

CASOS DE USO de fs.watch

  • Recargar configuración automáticamente cuando se edita un archivo
  • Vigilar subidas de archivos por usuarios
  • Monitorizar logs en tiempo real
  • Construir herramientas de desarrollo (tipo “auto-reload”)
  • Detectar cambios en carpetas de proyectos para automatizaciones.

Transform Streams (streams que modifican datos en tiempo real)

Los Transform Streams permiten transformar los datos mientras fluyen.

Ejemplos típicos: convertir texto a mayúsculas, sustituir palabras, comprimir o descomprimir archivos, cifrar o descifrar datos, formatear JSON, filtrar líneas, limpiar texto, etc.

Son muy potentes en pipelines.

Ejemplo 1: Transform Stream que convierte texto a mayúsculas

// EJEMPLO 1: Transform Stream simple que convierte todo el texto a MAYÚSCULAS.
//
// Este ejemplo demuestra la idea fundamental:
// - leemos desde un stream de entrada
// - transformamos los chunks
// - los escribimos en otro stream

import fs from "fs";
import path from "path";
import { Transform } from "stream";

const origen = path.join(process.cwd(), "archivos", "texto.txt");
const destino = path.join(process.cwd(), "salidas", "mayusculas.txt");

// Creamos un Transform Stream personalizado
const convertirMayus = new Transform({
transform(chunk, encoding, callback) {
// chunk es un Buffer. Lo convertimos a string, aplicamos cambios,
// y volvemos a entregarlo como Buffer.
const texto = chunk.toString().toUpperCase();
callback(null, texto);
}
});

// Creamos streams normales de lectura y escritura
const leer = fs.createReadStream(origen);
const escribir = fs.createWriteStream(destino);

// Conectamos todo: lectura → transformación → escritura
leer.pipe(convertirMayus).pipe(escribir);

// Evento final
escribir.on("finish", () => {
console.log("Archivo transformado a mayúsculas correctamente.");
});

Ejemplo 2: Transform Stream que filtra líneas con una palabra concreta

// EJEMPLO 2: Transform stream que filtra líneas con una palabra específica.
// Muy útil para trabajar con logs grandes sin cargarlos en memoria.

import fs from "fs";
import { Transform } from "stream";
import readline from "readline";
import path from "path";

const origen = path.join(process.cwd(), "logs", "server.log");
const destino = path.join(process.cwd(), "salidas", "solo-errores.log");

// Transform stream que solo deja pasar líneas que contengan la palabra "ERROR"
const filtrarErrores = new Transform({
transform(chunk, encoding, callback) {
const texto = chunk.toString();
const lineas = texto.split("\n");

// Filtramos las líneas que contienen "ERROR"
const soloErrores = lineas
.filter(linea => linea.includes("ERROR"))
.join("\n");

callback(null, soloErrores);
}
});

const leer = fs.createReadStream(origen);
const escribir = fs.createWriteStream(destino);

leer.pipe(filtrarErrores).pipe(escribir);

escribir.on("finish", () => {
console.log("Archivo filtrado: solo líneas con ERROR.");
});

CASOS DE USO de Transform Streams

  • Procesar logs enormes en tiempo real.
  • Sanitizar datos entrantes (ej., eliminar palabras o limpiar entradas).
  • Convertir formatos (mayúsculas, minúsculas, JSON → CSV, etc.).
  • Crear pipelines de procesamiento de datos.
  • Trabajar con compresión (gzip, brotli).
  • Construir flujos de subida/descarga que modifiquen el contenido.

Conclusión

El módulo de sistema de archivos de Node.js con ES Modules proporciona una API poderosa y moderna para trabajar con archivos y directorios. Las operaciones basadas en promesas permiten un código más limpio y legible usando async/await.

Los conceptos clave cubiertos incluyen:

  • Lectura y escritura de archivos de texto y binarios
  • Operaciones con directorios (creación, listado, eliminación)
  • Uso del módulo path para manejo seguro de rutas
  • Trabajo eficiente con archivos grandes mediante streams
  • Operaciones avanzadas como copia, movimiento y cambio de permisos

Este conocimiento forma la base para construir aplicaciones que necesitan interactuar con el sistema de archivos, como herramientas de línea de comandos, procesadores de datos, servidores de archivos y sistemas de backup.