- Published on
Understanding Streams in Node.js
- Authors
- Name
- Ganesh Negi

What is streams
Streams in Node.js help move data piece by piece (instead of all at once) from one place to another, preventing Out-of-Memory Errors when handling large files.
Example with Scenario
πͺ£ Think of it Like Buckets & Pipes
Imagine you have two buckets: πΉ Source bucket β Full of water (data). πΉ Destination bucket β Empty and needs to be filled.
How to Transfer Water? 1οΈβ£ Using a Buffer (Old Way):
Take all the water from the source into a smaller buffer bucket. Carry the buffer bucket and pour everything into the destination. Problem? If the buffer bucket is too small, it overflows. 2οΈβ£ Using a Stream (Efficient Way):
Instead of carrying everything at once, use a hose pipe (stream). Water (data) flows gradually from the source to the destination without overflow. π In Node.js, Streams act like this hose pipe! They transfer data in small chunks, making the process fast, memory-efficient, and smooth.
π Why Use Streams in Node.js?
β No Memory Overload β Handles large files without consuming too much memory. β Faster Processing β Data flows continuously, no waiting for full file loads. β Ideal for Big Files β Great for videos, logs, file uploads, and real-time data processing.
Understanding Readable Streams in Node.js
A readable stream is a way to read data bit by bit from a source and send it in small chunks to a destination. Instead of loading the entire file or dataset at once, streams process data gradually, making them efficient and memory-friendly.
πΉ How Readable Streams Work? 1οΈβ£ Reads data from a source (file, database, API, etc.). 2οΈβ£ Emits βdataβ events β Sends data chunk by chunk to the next stage. 3οΈβ£ Emits an βendβ event when all data has been read. 4οΈβ£ Handles errors using the βerrorβ event.
β Example: Creating a Readable Stream from an Array
const { Readable } = require("stream");
const dataArray = ["Hello", "World", "Streams", "in", "Node.js"];
const readStream = new Readable({
read() {
this.push(dataArray.length ? dataArray.shift() + " " : null);
},
});
readStream.on("data", (chunk) => {
console.log("Received chunk:", chunk.toString());
});
readStream.on("end", () => {
console.log("Stream finished reading!");
});
readStream.on("error", (err) => {
console.error("Error:", err);
});
πΉ What Happens Here? β The readStream reads words one by one and emits them as βdataβ events. β When the array is empty, it emits an βendβ event, signaling the stream is complete. β If any issue occurs, the βerrorβ event is triggered.
π Types of Data Read in Streams
Streams can read data in three different modes:
πΈ Binary Mode (Default) β Reads raw binary data. πΈ String Mode β Reads data as a string (set encoding: "utf-8"). πΈ Object Mode β Reads entire objects instead of just strings/binary (set objectMode: true).
Example: Reading as String
const fs = require("fs");
const readStream = fs.createReadStream("file.txt", { encoding: "utf-8" });
readStream.on("data", (chunk) => {
console.log("String chunk:", chunk);
});
π― Why Use Readable Streams? β Handles large data without memory overload β Faster processing & better performance β Useful for reading files, APIs, real-time data, and logs
With readable streams, data flows efficiently instead of waiting for everything to load at once! π
Understanding Writable Streams in Node.js
A writable stream is the destination for data. It receives data in chunks and writes it to a file, database, or any other output. Just like readable streams, writable streams process data gradually instead of all at once, making them efficient for handling large data.
πΉ How Writable Streams Work? 1οΈβ£ Receives data from a readable stream or another source. 2οΈβ£ Writes data in chunks using the .write() method. 3οΈβ£ Ends the stream with .end() when writing is complete. 4οΈβ£ Emits events like βfinishβ and βerrorβ for handling results.
β Example: Writing Data to a File Using a Writable Stream
const fs = require("fs");
const writeStream = fs.createWriteStream("output.txt");
writeStream.write("Hello, this is the first chunk.\n");
writeStream.write("Here comes another piece of data.\n");
writeStream.end("Final chunk of data. Stream is ending.\n");
writeStream.on("finish", () => {
console.log("Write stream finished!");
});
writeStream.on("error", (err) => {
console.error("Error:", err);
});
πΉ What Happens Here? β writeStream.write() writes data in chunks to output.txt. β writeStream.end() signals the end of the writing process. β The βfinishβ event confirms when writing is complete. β The βerrorβ event handles any writing issues.
π Common Writable Stream Methods πΈ .write(data) β Writes a chunk of data. πΈ .end(data?) β Closes the stream (optional final data). πΈ .on('finish', callback) β Triggered when writing is complete. πΈ .on('error', callback) β Catches errors during writing.
β Example: Piping Data from a Readable to a Writable Stream
const readStream = fs.createReadStream("input.txt");
const writeStream = fs.createWriteStream("output.txt");
readStream.pipe(writeStream);
π₯ Super Efficient! Instead of manually reading and writing chunks, the .pipe() method automatically transfers data from the readable stream to the writable stream.
π― Why Use Writable Streams? β Handles large files efficiently β Minimizes memory usage β Essential for logging, saving files, and data processing
Writable streams make storing and processing data seamless, ensuring your Node.js applications run smoothly and efficiently! π
Understanding Backpressure in Node.js Streams
Backpressure occurs when a writable stream cannot keep up with the speed of an incoming readable stream. This can cause performance issues, memory overload, or even data loss.
πͺ£ Think of It Like a Funnel & Hose Imagine a hose (readable stream) pouring water into a funnel (writable stream) connected to a bucket (destination).
π° What happens if you pour water too fast? πΉ The funnel gets full and water starts spilling over. πΉ You must pause pouring until the funnel clears.
π In Node.js, when a writable stream canβt handle incoming data fast enough, it creates backpressure.
β How Backpressure Works in Node.js? The .write() method in a writable stream returns true or false: β true β Stream can accept more data. β false β Stream is full; pause the readable stream.
Example: Handling Backpressure Properly
const fs = require("fs");
const readStream = fs.createReadStream("largeFile.txt");
const writeStream = fs.createWriteStream("output.txt");
readStream.on("data", (chunk) => {
if (!writeStream.write(chunk)) {
console.log("Backpressure detected! Pausing readStream...");
readStream.pause();
}
});
writeStream.on("drain", () => {
console.log("Writable stream ready again. Resuming readStream...");
readStream.resume();
});
readStream.on("end", () => {
writeStream.end();
console.log("File processing complete!");
});
πΉ What Happens Here? β Listens for βdataβ events from readStream. β If writeStream.write(chunk) returns false, it pauses readStream. β When the writable stream is ready again (drain event), it resumes reading.
π― Why is Backpressure Important? β Prevents memory overload β Ensures writable streams don't get overwhelmed. β Avoids data loss β Keeps data flow controlled and efficient. β Optimizes performance β Helps Node.js handle large files smoothly.
Backpressure balances data flow, preventing slow writable streams from becoming a bottleneck! π
Piping Streams in Node.js
Handling readable and writable streams manually requires listening to multiple events like data, drain, and end. But there's a simpler wayβusing the pipe() method!
π What is pipe()? πΉ The pipe() method connects a readable stream directly to a writable stream. πΉ It automatically handles backpressure, ensuring smooth data flow. πΉ The only thing we need to manage is error handling.
β Example: Copying a File Using pipe()
const fs = require("fs");
const readStream = fs.createReadStream("input.txt");
const writeStream = fs.createWriteStream("output.txt");
readStream.pipe(writeStream);
writeStream.on("finish", () => {
console.log("File copied successfully!");
});
writeStream.on("error", (err) => {
console.error("Error:", err);
});
πΉ What Happens Here? β readStream.pipe(writeStream) automatically transfers data in chunks. β No need to manually pause, resume, or listen for drain eventsβitβs all handled internally. β If an error occurs, we catch it using .on("error").
π― Why Use pipe()? β Less code, fewer bugs β No need to manually manage backpressure. β Memory efficient β Processes data chunk by chunk instead of loading it all at once. β Fast and optimized β Ideal for large file transfers, APIs, and data processing.
With pipe(), streams become super easy to use, making your Node.js applications more efficient and maintainable! π
Understanding Duplex Streams in Node.js
A duplex stream is a special type of stream that is both readable and writable at the same time. It acts as a middle layer between a readable and a writable stream, allowing data to flow through it bidirectionally.
π How Duplex Streams Work? β Reads data from an input stream (like a readable stream). β Processes or modifies the data (optional). β Writes the processed data to an output stream (like a writable stream).
Duplex vs. Transform Streams πΉ A duplex stream does not necessarily modify the dataβit just passes it along. πΉ A transform stream (a special type of duplex stream) changes the data before passing it forward.
β Example: Creating a Custom Duplex Stream
const { Duplex } = require("stream");
class MyDuplexStream extends Duplex {
constructor() {
super();
this.data = [];
}
_write(chunk, encoding, callback) {
console.log(`Writing: ${chunk.toString()}`);
this.data.push(chunk);
callback();
}
_read(size) {
if (this.data.length === 0) {
this.push(null); // Signal end of stream
} else {
this.push(this.data.shift());
}
}
}
const duplexStream = new MyDuplexStream();
duplexStream.write("Hello, ");
duplexStream.write("this is a duplex stream!");
duplexStream.on("data", (chunk) => {
console.log(`Reading: ${chunk.toString()}`);
});
duplexStream.end();
πΉ What Happens Here? β write() stores data in an internal array. β read() retrieves and emits data chunk by chunk. β Acts as both a readable and a writable stream.
π― When to Use Duplex Streams? β Data relay β Forward data from one stream to another. β Network communication β Handles bidirectional data flow, like sockets. β Custom processing β Stores and processes data before passing it along.
Duplex streams power advanced pipelines in Node.js, making data handling flexible and efficient! π
Understanding Transform Streams in Node.js
A transform stream is a special type of duplex stream that modifies the data as it passes through. Unlike a normal duplex stream, which just forwards data, a transform stream changes the data before writing it to the output.
π How Transform Streams Work? β Receives data from a readable stream. β Processes and modifies the data. β Writes the transformed data to a writable stream.
β Example: Replacing Vowels with βXβ in a Transform Stream
const { Transform } = require("stream");
class ReplaceVowelsStream extends Transform {
_transform(chunk, encoding, callback) {
const modifiedData = chunk
.toString()
.replace(/[aeiouAEIOU]/g, "X"); // Replace vowels with 'X'
this.push(modifiedData); // Send modified data to writable stream
callback();
}
}
const transformStream = new ReplaceVowelsStream();
process.stdin.pipe(transformStream).pipe(process.stdout);
πΉ What Happens Here? β process.stdin.pipe(transformStream).pipe(process.stdout);
Reads input from the console (stdin). Replaces vowels in the text using ReplaceVowelsStream. Outputs the modified text to the console (stdout). Example Output
Input: Hello World!
Output: HXllX WXrld!
π― When to Use Transform Streams? β Data transformation β Modify text, compress files, encrypt/decrypt data. β File processing β Convert text files, reformat JSON, CSV parsing. β Real-time modifications β Modify network requests, manipulate API responses.
Transform streams simplify data processing in Node.js, making pipelines more powerful and efficient! π