Skip to content

Node.js Streams — Complete Guide to Stream API, Pipe, and Data Processing

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Node.js Streams. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js streams Process data in chunks, enabling memory-efficient handling of large files, network responses, and data transformations without loading everything into memory.

What You'll Learn

By the end of this tutorial, you'll use readable, writable, and transform streams, pipe data between streams, handle backpressure, and process large datasets efficiently.

Why Streams Matter

Loading a 1GB file into memory crashes a server. Streams process data piece by piece, using constant memory regardless of file size. This is critical for video processing, log analysis, and file uploads in tools like DodaZIP.

Real-World Use

A video transcoding service reads a large video file via a readable stream, compresses it through a transform stream, and writes the output to disk, using less than 50MB of memory for a 4GB file.

Streams Learning Path

flowchart LR
  A[File System] --> B[Streams]
  B --> C[Buffers]
  C --> D[Events]
  D --> E[Express.js]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Types of Streams

import { Readable, Writable, Transform, Duplex, PassThrough } from "node:stream";
Type Description
Readable Source of data (file read, HTTP request)
Writable Destination for data (file write, HTTP response)
Transform Modifies data in transit (compression, encryption)
Duplex Both readable and writable (TCP socket)
PassThrough Passes data through unchanged (monitoring)

Reading from a Stream

import fs from "node:fs";
const readStream = fs.createReadStream("large-file.txt", { encoding: "utf8", highWaterMark: 16384 });
readStream.on("data", (chunk) => {
  console.log(`Received ${chunk.length} bytes`);
});
readStream.on("end", () => {
  console.log("File read complete");
});
readStream.on("error", (err) => {
  console.error("Stream error:", err.message);
});

Writing to a Stream

import fs from "node:fs";
const writeStream = fs.createWriteStream("output.txt");
writeStream.write("First line\n");
writeStream.write("Second line\n");
writeStream.end("Final line\n");
writeStream.on("finish", () => console.log("Write complete"));

Piping Streams

Pipe connects a readable stream to a writable stream, managing backpressure automatically.

import fs from "node:fs";
import zlib from "node:zlib";
const readStream = fs.createReadStream("input.txt");
const gzipTransform = zlib.createGzip();
const writeStream = fs.createWriteStream("input.txt.gz");
readStream.pipe(gzipTransform).pipe(writeStream);
writeStream.on("finish", () => console.log("File compressed"));

Transform Stream Example

A transform stream modifies data as it passes through.

import { Transform } from "node:stream";
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});
process.stdin.pipe(upperCaseTransform).pipe(process.stdout);

Backpressure

When a readable stream pushes data faster than the writable stream can handle, backpressure occurs. The pipe method handles this automatically.

const readable = getReadableStream();
const writable = getWritableStream();
readable.pipe(writable); // Automatic backpressure management
readable.on("error", () => writable.destroy());
writable.on("error", () => readable.destroy());

Stream Utilities

import { pipeline } from "node:stream/promises";
import fs from "node:fs";
import zlib from "node:zlib";
try {
  await pipeline(
    fs.createReadStream("input.txt"),
    zlib.createGzip(),
    fs.createWriteStream("input.txt.gz")
  );
  console.log("Pipeline completed");
} catch (err) {
  console.error("Pipeline failed:", err);
}

Pipeline with promises provides cleaner error handling than manual piping.

Common Mistakes

1. Not Handling Errors on Streams

An unhandled error on a stream crashes the process. Always add error listeners or use pipeline.

2. Reading Entire Stream Into Memory

Stream chunks accumulate if you collect them in an array. Process data chunk by chunk instead.

3. Forgetting to Call callback() in Transform

The transform function must call callback() or data flow stops silently.

4. Ignoring Backpressure

Without pipe or proper drain handling, memory usage grows unbounded when writable is slower than readable.

5. Creating Streams Without highWaterMark

Default buffer size (16KB for streams, 64KB for files) may not suit your data size profile.

Practice Questions

1. What is backpressure in streams?

Backpressure occurs when the writable stream cannot keep up with the readable stream. pipe() manages this automatically by pausing the readable.

2. What is the difference between pipe and pipeline?

pipe returns the writable stream for chaining but doesn't forward errors. pipeline manages cleanup and error propagation correctly.

3. How do you create a custom transform stream?

Extend Transform class or pass a transform function to the constructor. Call this.push() with transformed data.

4. When should you use a stream instead of readFile?

Use streams for large files (over 100MB), continuous data, or when memory is constrained.

5. Challenge: Create a transform stream that counts lines passing through it.

import { Transform } from "node:stream";
class LineCounter extends Transform {
  constructor() { super({ objectMode: true }); this.lines = 0; }
  _transform(chunk, encoding, callback) {
    this.lines += chunk.toString().split("\n").length - 1;
    this.push(chunk);
    callback();
  }
  _flush(callback) {
    this.push(`\n--- Total lines: ${this.lines} ---\n`);
    callback();
  }
}
process.stdin.pipe(new LineCounter()).pipe(process.stdout);

FAQ

What is the highWaterMark option?

It controls the internal buffer size (in bytes). When buffered data exceeds this, the stream pauses the source.

Can streams be paused and resumed?

Yes. readable.pause() stops data events. readable.resume() restarts them.

What is objectMode in streams?

By default, streams work with Buffer/string. Object mode allows streaming JavaScript objects.

How do I handle errors in piped streams?

Use pipeline() from stream/promises which handles cleanup. Manual pipe() requires error listeners on each stream.

What is the difference between Duplex and Transform?

Duplex is both readable and writable independently. Transform modifies data between reading and writing.

Mini Project: CSV Processor

Build a transform stream that reads a CSV file, converts it to JSON lines, and writes the output.

import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import fs from "node:fs";
class CsvToJson extends Transform {
  constructor() {
    super({ objectMode: true });
    this.headers = null;
  }
  _transform(chunk, encoding, callback) {
    const line = chunk.toString().trim();
    if (!line) return callback();
    if (!this.headers) {
      this.headers = line.split(",");
      return callback();
    }
    const values = line.split(",");
    const obj = {};
    this.headers.forEach((h, i) => obj[h] = values[i]);
    this.push(JSON.stringify(obj) + "\n");
    callback();
  }
}
await pipeline(
  fs.createReadStream("data.csv"),
  new CsvToJson(),
  fs.createWriteStream("data.json")
);

What's Next

Node.js Buffers Node.js Events Express.js

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro