Por qué es importante el manejo de archivos
Los servidores leen y escriben archivos constantemente: configuraciones, subidas, logs, cachés, reportes generados y resultados de compilación. El módulo node:fs de Node.js es la interfaz, y la forma en que lo utilices determinará si tu servidor se mantiene responsivo y si tus datos permanecen seguros.
Hay tres aspectos fundamentales. Utiliza la promise API para que el event loop permanezca libre. Construye las rutas de forma segura para evitar que la entrada del usuario pueda salir de un directorio. Y utiliza streams para archivos grandes para que el consumo de memoria no aumente según el tamaño del archivo.
Lectura y escritura de archivos
La API de promesas es la opción predeterminada para el código de la aplicación.
// files.js
import { readFile, writeFile, appendFile } from "node:fs/promises";
// text
const text = await readFile("notes.md", "utf8");
await writeFile("notes.md", text + "\nnew line\n", "utf8");
await appendFile("app.log", "event\n", "utf8");
// binary
const image = await readFile("photo.png"); // Buffer
await writeFile("copy.png", image);
Pasa una codificación como "utf8" para obtener un string; omítela para obtener un Buffer de bytes raw. writeFile reemplaza el archivo, mientras que appendFile añade contenido a este. Ninguno de los dos crea directorios faltantes, por lo que debes llamar a mkdir primero cuando sea necesario.
Directorios
// dirs.js
import { mkdir, readdir, rm, rename, stat } from "node:fs/promises";
await mkdir("data/cache", { recursive: true });
const entries = await readdir("data", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) console.log("dir", entry.name);
}
const info = await stat("data/cache");
console.log(info.size, info.mtime);
await rename("data/tmp.json", "data/final.json");
await rm("data/old", { recursive: true, force: true });
recursive: true hace que mkdir y rm operen sobre árboles completos, y withFileTypes devuelve entradas que puedes validar sin llamadas adicionales a stat. Usa stat para verificar la existencia, el tamaño y las marcas de tiempo, pero ten en cuenta que sigue los symlinks; lstat no lo hace.
Rutas
El módulo node:path se encarga de la manipulación de rutas de forma portable.
// paths.js
import path from "node:path";
const file = path.join("uploads", "avatars", "ada.png");
const absolute = path.resolve("uploads", "ada.png");
const name = path.basename(file); // ada.png
const ext = path.extname(file); // .png
const dir = path.dirname(file); // uploads/avatars
Utiliza path.join y path.resolve en lugar de la concatenación de strings, y import.meta.dirname (o import.meta.url en versiones anteriores) para localizar archivos relativos al módulo actual. En Windows, path utiliza barras invertidas, razón por la cual la concatenación manual falla.
Seguridad: path traversal
La vulnerabilidad de archivos más común es permitir que la entrada del usuario escape del directorio previsto.
// safe-path.js
import path from "node:path";
const BASE = "/srv/uploads";
function safePath(name) {
const target = path.resolve(BASE, name);
if (target !== BASE && !target.startsWith(BASE + path.sep)) {
throw new Error("Invalid path");
}
return target;
}
Resuelve la entrada contra una base fija y luego verifica que el resultado siga estando dentro de ella. Asimismo, valida los nombres de archivo, rechaza los bytes nulos y nunca expongas errores crudos del sistema de archivos a los clientes. Consulta la guía de Web Security.
Escrituras atómicas
Si ocurre un fallo durante la escritura, un lector podría ver un archivo escrito a medias. Para el estado y la configuración, escribe en un archivo temporal y renómbralo en su lugar.
// atomic.js
import { writeFile, rename } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const tmp = `/srv/state/.${randomUUID()}.tmp`;
await writeFile(tmp, JSON.stringify(state), "utf8");
await rename(tmp, "/srv/state/current.json");
rename es atómico dentro del mismo sistema de archivos, por lo que los lectores siempre verán el archivo antiguo o el nuevo completo.
Streaming de archivos grandes
Para cualquier contenido que pueda ser voluminoso, utiliza streaming en lugar de buffering.
// stream.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("big.csv"),
createWriteStream("copy.csv"),
);
El streaming mantiene el uso de memoria estable y permite comenzar el procesamiento antes de que se haya leído el archivo completo. La guía de Streams cubre transforms, backpressure y object mode.
Monitoreo de archivos
// watch.js
import { watch } from "node:fs";
const watcher = watch("./config", { recursive: true }, (event, filename) => {
console.log(event, filename);
});
process.on("SIGINT", () => watcher.close());
Los watchers son ideales para servidores de desarrollo y recargas de configuración. Implementa un debounce para ráfagas de eventos, gestiona eliminaciones y renombrados, y cierra siempre el watcher para que el proceso pueda finalizar.
Mejores prácticas
- Usa
node:fs/promisesen el código de la aplicación. - Lee y escribe con una codificación explícita, o espera un Buffer.
- Construye rutas con
node:pathy resuélvelas respecto a una base conocida. - Protégete contra el path traversal en cualquier nombre de archivo proporcionado por el usuario.
- Usa streams para archivos grandes; carga en buffer solo lo que necesites completo.
- Escribe archivos de estado de forma atómica usando un archivo temporal y renombrándolo.
- Cierra los watchers y los file handles para que los procesos puedan finalizar correctamente.
Errores comunes
- Usar
readFileSyncen un request handler. - Concatenar la entrada del usuario en las rutas (paths).
- Asumir que un directorio existe antes de escribir.
- Leer un archivo enorme en memoria y provocar un crash bajo carga.
- Exponer errores crudos del sistema de archivos y filtrar rutas.
- Olvidar cerrar los watchers o streams.
Próximos pasos
El manejo de archivos es una tarea cotidiana en el servidor. Profundiza con los Streams, comprende la planificación en la guía del Event Loop y refuerza la seguridad de tus rutas con la guía de Web Security.