Leer un archivo de configuración es trivial. Procesar un log de 5 GB, exportar un millón de filas a CSV o enviar un video a un cliente es otra historia: si intentás cargarlo todo en memoria, el proceso se cae. Los streams son la respuesta de Node.js a ese problema, y una de las razones por las que es tan eficiente manejando datos. En el libro Node.js desde cero los vemos junto con el sistema de archivos; acá tenés los patrones que más vas a usar.
El módulo fs con promesas
Para archivos de tamaño razonable, la API de promesas es la más cómoda:
import { readFile, writeFile, appendFile, mkdir, readdir } from 'node:fs/promises';
import { join } from 'node:path';
const dir = join(import.meta.dirname, 'datos');
await mkdir(dir, { recursive: true }); // no falla si ya existe
await writeFile(join(dir, 'notas.txt'), 'Primera línea\n', 'utf8');
await appendFile(join(dir, 'notas.txt'), 'Segunda línea\n');
const contenido = await readFile(join(dir, 'notas.txt'), 'utf8');
console.log(contenido);
// Leer y escribir JSON
await writeFile(join(dir, 'config.json'), JSON.stringify({ puerto: 3000 }, null, 2));
const config = JSON.parse(await readFile(join(dir, 'config.json'), 'utf8'));
console.log(await readdir(dir)); // [ 'config.json', 'notas.txt' ]
path.join() para armar rutas: elige el separador correcto en Windows y en Linux. Y evitá
las versiones síncronas (readFileSync) dentro de un servidor, porque
bloquean el event loop.
Por qué existen los streams
readFile carga el archivo entero en memoria antes de devolverlo. Con un archivo de 5 GB, eso
significa 5 GB de RAM (o un error). Un stream, en cambio, entrega los datos en trozos
pequeños a medida que se leen, y la memoria usada se mantiene constante sin importar el tamaño.
| Tipo | Qué hace | Ejemplos |
|---|---|---|
| Readable | Produce datos | fs.createReadStream, la petición HTTP (req) |
| Writable | Consume datos | fs.createWriteStream, la respuesta HTTP (res) |
| Transform | Recibe datos, los transforma y los emite | zlib.createGzip, un parser de CSV |
Los streams también gestionan la contrapresión (backpressure): si quien escribe es más lento que quien lee, la lectura se pausa automáticamente hasta que haya espacio.
Ejemplo: comprimir un archivo con pipeline
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
const [origen = 'ventas.csv'] = process.argv.slice(2);
// leer → comprimir → escribir, trozo a trozo y con memoria constante
await pipeline(
createReadStream(origen),
createGzip(),
createWriteStream(`${origen}.gz`),
);
console.log(`Comprimido: ${origen}.gz`);
pipeline conecta los streams, propaga los errores de cualquiera de ellos y cierra todos al
terminar o al fallar. Usalo en lugar del antiguo .pipe(), que no limpia correctamente si
algo sale mal.
Ejemplo: procesar un log de varios GB línea a línea
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const lineas = createInterface({
input: createReadStream('access.log'),
crlfDelay: Infinity, // trata \r\n como un solo salto de línea
});
const porCodigo = {};
for await (const linea of lineas) {
// formato típico de Nginx: ... "GET /ruta HTTP/1.1" 404 512 ...
const codigo = linea.match(/" (\d{3}) /)?.[1];
if (codigo) porCodigo[codigo] = (porCodigo[codigo] ?? 0) + 1;
}
console.table(porCodigo); // { 200: 98123, 304: 1200, 404: 312, 500: 7 }
Con for await, el código parece un bucle normal, pero cada línea se lee bajo demanda: el
archivo puede pesar 50 MB o 50 GB, y la memoria apenas cambia.
Streams en un servidor HTTP
En Node.js, req y res ya son streams. Eso permite servir archivos grandes sin
cargarlos en memoria:
import { createServer } from 'node:http';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { pipeline } from 'node:stream/promises';
createServer(async (req, res) => {
try {
const archivo = 'informe-anual.pdf';
const { size } = await stat(archivo);
res.writeHead(200, { 'Content-Type': 'application/pdf', 'Content-Length': size });
await pipeline(createReadStream(archivo), res);
} catch {
if (!res.headersSent) res.writeHead(500);
res.end();
}
}).listen(3000);
En Express, res.download() y res.sendFile() hacen esto mismo por vos (ver
tu primer servidor con Express).
Errores comunes
- Usar
readFilecon archivos que pueden crecer (logs, exportaciones, subidas de usuarios): hoy funciona, el día que el archivo pese 2 GB, no. - No manejar errores de los streams: un stream sin manejador de error puede tumbar el
proceso.
pipelinelo resuelve. - Construir rutas con datos del usuario sin validar: un nombre como
../../etc/passwdpermite leer archivos del servidor. Validá y normalizá las rutas. - Olvidar la codificación: sin
'utf8',readFiledevuelve un Buffer, no un texto.
Domina la entrada y salida en Node.js
El libro Node.js desde cero explica el sistema de archivos y los streams con ejemplos prácticos, antes de pasar a servidores con Express.
Comprar en Amazon