Streams, Events, and HTTP
Node.js is often used for programs that handle data over time.
Examples:
- reading a large file
- receiving an HTTP request body
- sending a response
- processing logs
- handling realtime messages
Streams and events are two important Node patterns for this kind of work.
EventEmitter Basics
Node's EventEmitter lets objects publish and listen for named events.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("ready", () => {
console.log("Ready event received");
});
emitter.emit("ready");This is similar to browser events in concept, but it is not the DOM event system.
There is no bubbling or capturing.
It is just an object emitting named events.
Event Data
Events can pass data to listeners.
import { EventEmitter } from "node:events";
const bus = new EventEmitter();
bus.on("user:created", (user) => {
console.log(`Created ${user.name}`);
});
bus.emit("user:created", { id: 1, name: "Ada" });Event emitters are useful when a piece of code should announce that something happened without directly calling every listener.
The error Event
In Node, error events are special for many emitters.
If an EventEmitter emits error and there is no listener, Node may throw.
emitter.on("error", (error) => {
console.error("Emitter error:", error.message);
});When working with streams, servers, or custom emitters, pay attention to error events.
What Is a Stream?
A stream represents data that arrives or leaves in chunks.
Instead of loading everything at once, a stream processes data piece by piece.
This is useful for:
- large files
- network data
- request bodies
- response bodies
- logs
For small files, readFile() is often fine.
For large data, streams can use less memory.
Reading with a Stream
import { createReadStream } from "node:fs";
const stream = createReadStream("large-file.txt", {
encoding: "utf8",
});
stream.on("data", (chunk) => {
console.log("Received chunk:", chunk.length);
});
stream.on("end", () => {
console.log("Finished reading");
});
stream.on("error", (error) => {
console.error("Stream error:", error.message);
});The file is read in chunks.
Each chunk triggers a data event.
Writing with a Stream
import { createWriteStream } from "node:fs";
const stream = createWriteStream("output.txt", {
encoding: "utf8",
});
stream.write("First line\n");
stream.write("Second line\n");
stream.end("Done\n");Writable streams are useful when output is built over time.
Piping Streams
Piping sends data from a readable stream to a writable stream.
import { createReadStream, createWriteStream } from "node:fs";
const source = createReadStream("input.txt");
const destination = createWriteStream("copy.txt");
source.pipe(destination);This copies data without manually handling every chunk.
For robust production code, use pipeline utilities that handle errors and cleanup.
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
await pipeline(
createReadStream("input.txt"),
createWriteStream("copy.txt")
);HTTP Servers
Node can create HTTP servers with the built-in http module.
import http from "node:http";
const server = http.createServer((request, response) => {
response.setHeader("Content-Type", "text/plain");
response.end("Hello from Node\n");
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});When a request arrives, Node calls your request handler.
This is event-driven programming.
Request and Response Objects
In the built-in HTTP module:
requestcontains information about the incoming requestresponseis used to send data back
const server = http.createServer((request, response) => {
console.log(request.method);
console.log(request.url);
response.statusCode = 200;
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify({ ok: true }));
});The request object is a readable stream.
The response object is a writable stream.
That is why streams matter in Node web servers.
Simple Routing
The built-in http module does not provide full routing.
You can write basic routing manually:
const server = http.createServer((request, response) => {
if (request.method === "GET" && request.url === "/health") {
response.statusCode = 200;
response.end("OK");
return;
}
response.statusCode = 404;
response.end("Not found");
});For real apps, frameworks provide routing, middleware, validation, and error handling.
Practical Pitfalls
Long CPU-heavy work blocks Node's event loop.
// A very expensive loop can delay timers and requests
for (let i = 0; i < 1_000_000_000; i += 1) {}Node is good at asynchronous I/O, but JavaScript still runs on the main thread.
Use care with:
- large synchronous loops
- synchronous file APIs in servers
- unbounded request bodies
- missing stream error handlers
- keeping too much data in memory
Security Cautions
When building Node servers:
- validate request input
- do not expose stack traces to users
- do not trust file paths from requests
- do not log secrets
- set appropriate response headers
- use HTTPS in production
- keep dependencies updated
Node gives you low-level power. Frameworks help, but they do not remove the need for secure decisions.
Common Mistakes
Do not use streams for every tiny file just because they exist.
Do not ignore error events on streams.
Do not assume Node HTTP routing works like Express routing.
Do not block the event loop with heavy synchronous work in a server.
Do not store request bodies in memory without size limits in real applications.
Summary
Node.js uses events and streams heavily.
EventEmitter lets objects publish events, streams process data in chunks, and the built-in http module shows how Node handles web requests at a low level.
These ideas appear underneath many Node frameworks and libraries.