Lesson 27 of 50 – Streams

54% Completed
54%

Streams in Node.js

Streams allow Node.js to process data piece by piece instead of loading the entire data into memory at once. They are especially useful for working with large files, network requests, and responses.

Remember: A stream processes data in chunks. This can reduce memory usage and allow data processing to begin before the entire input is available.

1. What Is a Stream?

A stream is an interface for reading or writing data continuously in smaller pieces called chunks. Node.js provides stream support through the built-in stream module.

Streams are commonly used for:

  • Reading large files.
  • Writing data to files.
  • Processing uploaded files.
  • Sending HTTP responses.
  • Processing data from networks.
Example: Reading a Large File

Imagine a file containing several gigabytes of data. Reading the entire file at once may require a large amount of memory. A stream can read and process smaller chunks.

const fs = require("fs");

const stream = fs.createReadStream("large.txt");

stream.on("data", function(chunk) {
    console.log("Received a chunk");
});

stream.on("end", function() {
    console.log("File reading completed");
});

The data event is emitted whenever a chunk is available while the readable stream is flowing.

2. Types of Streams

Node.js provides four main types of streams.

Stream Type Purpose Example
Readable Reads data. fs.createReadStream()
Writable Writes data. fs.createWriteStream()
Duplex Reads and writes data. TCP socket
Transform Reads data, transforms it, and writes the result. Compression stream

3. Readable Streams

A readable stream supplies data that your program can consume. The fs.createReadStream() method creates a readable stream for a file.

Example: Reading a File with a Stream

Create a file named student.txt containing:

Welcome to Node.js Streams!

Now create a JavaScript file:

const fs = require("fs");

const readStream = fs.createReadStream("student.txt", {
    encoding: "utf8"
});

readStream.on("data", function(chunk) {
    console.log("Data:", chunk);
});

readStream.on("end", function() {
    console.log("Reading completed");
});

readStream.on("error", function(error) {
    console.log("Error:", error.message);
});
Example Output:
Data: Welcome to Node.js Streams!
Reading completed
The number and size of chunks depend on the stream settings and the data source. A small file may be delivered in one chunk.

4. Writable Streams

A writable stream allows your program to write data to a destination, such as a file. The fs.createWriteStream() method creates a writable file stream.

Example: Writing to a File
const fs = require("fs");

const writeStream = fs.createWriteStream("output.txt");

writeStream.write("Hello Student!\n");
writeStream.write("Learning Node.js Streams.\n");

writeStream.end();

writeStream.on("finish", function() {
    console.log("Writing completed");
});

writeStream.on("error", function(error) {
    console.log("Error:", error.message);
});
Output:
Writing completed

Content of output.txt:

Hello Student!
Learning Node.js Streams.

The finish event occurs after end() has been called and all data has been processed by the writable stream.

5. Piping Streams

The pipe() method sends data from a readable stream to a writable stream. It also helps manage the flow of data when the destination cannot accept it as quickly as the source produces it.

Example: Copying a File Using pipe()
const fs = require("fs");

const readStream = fs.createReadStream("student.txt");
const writeStream = fs.createWriteStream("copy.txt");

readStream.pipe(writeStream);

writeStream.on("finish", function() {
    console.log("File copied successfully");
});

readStream.on("error", function(error) {
    console.log("Read error:", error.message);
});

writeStream.on("error", function(error) {
    console.log("Write error:", error.message);
});
Output:
File copied successfully

This example copies the contents of student.txt into copy.txt. The source and destination files must be accessible.

6. Transform Streams

A transform stream is a special type of duplex stream. It receives input, modifies or processes it, and produces output.

Example: Converting Text to Uppercase
const { Transform } = require("stream");

const upperCaseStream = new Transform({
    transform(chunk, encoding, callback) {
        callback(null, chunk.toString().toUpperCase());
    }
});

upperCaseStream.on("data", function(data) {
    console.log(data.toString());
});

upperCaseStream.write("hello student");
upperCaseStream.end();
Output:
HELLO STUDENT

The transform() method receives a chunk and passes the transformed result to its callback.

7. Handling Stream Events

Streams emit events that indicate when data is available, when processing finishes, or when an error occurs.

Event Meaning
data A readable stream provides a chunk of data.
end A readable stream has no more data to provide.
finish A writable stream has processed all data after end().
error An error occurs in the stream.
close The stream or its underlying resource has closed.
drain A writable stream can accept more data after backpressure.
The end event belongs to readable streams, while finish is commonly used with writable streams. Do not assume they mean the same thing.

8. What Is Backpressure?

Backpressure occurs when data is produced faster than a writable stream can process it. Node.js provides mechanisms to avoid overwhelming the destination with too much buffered data.

Example: Handling Backpressure
const fs = require("fs");

const writeStream = fs.createWriteStream("output.txt");

let i = 0;

function writeData() {
    let canContinue = true;

    while (i < 100000 && canContinue) {
        i++;

        canContinue = writeStream.write(
            "This is line " + i + "\n"
        );
    }

    if (i < 100000) {
        writeStream.once("drain", writeData);
    } else {
        writeStream.end();
    }
}

writeStream.on("finish", function() {
    console.log("All lines written");
});

writeStream.on("error", function(error) {
    console.log("Error:", error.message);
});

writeData();
Output:
All lines written

When write() returns false, the program waits for drain before writing more data. This helps control memory usage.

9. Using pipeline()

Node.js provides pipeline() to connect streams and handle errors across the pipeline. It is often a safer choice than manually coordinating several streams.

Example: Copying a File with pipeline()
const fs = require("fs");
const { pipeline } = require("stream");

pipeline(
    fs.createReadStream("student.txt"),
    fs.createWriteStream("pipeline-copy.txt"),
    function(error) {
        if (error) {
            console.log("Pipeline failed:", error.message);
        } else {
            console.log("Pipeline completed successfully");
        }
    }
);
Output:
Pipeline completed successfully

The callback receives an error if the pipeline fails. Otherwise, it indicates that the pipeline completed.

10. Practical Assignment

Practice these programs:
  1. Create a readable stream to read a text file.
  2. Create a writable stream to write text to a file.
  3. Copy one file to another using pipe().
  4. Create a Transform stream that converts text to uppercase.
  5. Handle stream errors using the error event.
  6. Write a program that handles backpressure.
  7. Copy a file using pipeline().

Test Your Knowledge – Streams

Select the correct answer for each question.

1. What is the main benefit of using streams?
2. Which method connects a readable stream to a writable stream?
3. Which stream type can transform input data into output data?