logo
Published on

Understanding Streams in Node.js

Authors
  • avatar
    Name
    Ganesh Negi
    Twitter
Streams in Nodejs

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! πŸš€