streams — Streams
Processing data incrementally in Node.js — Readable, Writable, Transform streams, pipeline, and async iteration.
What it is, in plain English: Streams are Node.js's way of processing data piece by piece, without loading the whole thing into memory. A Readable stream produces data. A Writable stream consumes data. A Transform stream does both — it receives data, transforms it, and outputs the result. pipeline() connects streams together and handles errors and cleanup. Streams implement the async iterable protocol — you can use for await...of.
Where this bites you
Chunk boundaries ignore your data's structure. A file stream hands you 64 KB at a time, so a CSV row is cut in half whenever it straddles two chunks. A Transform that splits each chunk on "\n" on its own produces one wrong row from the first half and one garbage row from the second. Small test files fit in one chunk and never show it.
import { Transform } from "stream";
import { createReadStream } from "fs";
class CSVParser extends Transform {
constructor() {
super({ objectMode: true });
this.headers = null;
}
_transform(chunk, enc, cb) {
const lines = chunk.toString().split("\n").filter(Boolean); // each chunk on its own
for (const line of lines) {
const values = line.split(",");
if (!this.headers) { this.headers = values; continue; }
this.push(Object.fromEntries(this.headers.map((h, i) => [h, values[i]])));
}
cb();
}
}
// people.csv: a header + 5,000 rows, every city "Mumbai" (94 KB: two chunks, 64 KB + 30 KB)
let count = 0;
const broken = [];
createReadStream("people.csv").pipe(new CSVParser())
.on("data", (row) => { count++; if (row.city !== "Mumbai") broken.push(row); })
.on("end", () => { console.log("rows:", count); console.log("broken:", broken); });
// rows: 5001
// broken: [ { name: 'user3507', age: '27', city: 'Mum' },
// { name: 'bai', age: undefined, city: undefined } ]Why streams — memory-efficient data processing
Without streams, to process a large file you load it all into memory. A 10GB log file would need 10GB of RAM. Streams process data in small chunks — the same task uses only kilobytes of memory.
The four stream types
// Readable — source of data
// Writable — destination for data
// Transform — reads, transforms, writes (both ends)
// Duplex — separate read and write (like a TCP socket)
// You use streams constantly without realising it:
process.stdin; // Readable
process.stdout; // Writable
fs.createReadStream("file.txt"); // Readable
fs.createWriteStream("out.txt"); // Writable
zlib.createGzip(); // Transform
http.IncomingMessage; // Readable (req in createServer)
http.ServerResponse; // Writable (res in createServer)Reading a stream with async iteration
import { createReadStream } from "fs";
import { createInterface } from "readline";
// Read a large file line by line (memory-efficient):
const rl = createInterface({
input: createReadStream("large.csv"),
crlfDelay: Infinity,
});
let lineNumber = 0;
for await (const line of rl) {
lineNumber++;
if (lineNumber === 1) continue; // skip header
processRow(line.split(","));
}
console.log(`Processed ${lineNumber} lines`);pipeline — the correct way to connect streams
import { createReadStream, createWriteStream } from "fs";
import { createGzip } from "zlib";
import { pipeline } from "stream/promises";
// Compress a file:
await pipeline(
createReadStream("big-file.json"), // read
createGzip(), // compress
createWriteStream("big-file.json.gz") // write
);
// pipeline: handles backpressure, errors, and cleanup automatically
// ✗ Don't use .pipe() directly — it doesn't handle errors:
// readStream.pipe(gzip).pipe(writeStream); // if gzip fails, streams don't close!
Try It Yourself
Official Sources
- Node.js Docs — stream
- Node.js Docs — stream.pipeline()
- Node.js Docs — Streams — Backpressuring in Streams
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.