Knowledge/Node.js/Introduction
20 Chapters
LTS Baseline
Living Document
Free License
Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
JS
Engineering Notes · Open Knowledge Repository

Node.js — Engineering
Notes for Server Code

A working reference for Node as it runs in production — the event loop and why it matters, streams and backpressure, the module systems, and the operational concerns that keep a service alive under load.

20
Chapters
LTS
Baseline
Living
Document
Free
License
Ch. 01
The Runtime
What Node actually is: V8 + libuv + a standard library, single-threaded by default.
1.1 — One thread, many connections

Node runs your JavaScript on a single thread. It scales not by threads-per-request but by never blocking that thread — every I/O call hands off to the OS (via libuv) and your code continues. The model only breaks when you do CPU-heavy work synchronously and starve the loop.

Shell
$ node app.js
$ node --watch app.js      # built-in reloader
$ node --env-file=.env app.js  # load env (20+)
1.2 — The golden rule
Never block the event loop. A synchronous loop over a million items, JSON.parse on a 50 MB string, or fs.readFileSync in a request handler freezes every connection until it finishes. Offload CPU work to a worker (Ch. 15).
Ch. 02
Modules
CommonJS vs ES Modules — and how Node decides which one a file is.
2.1 — The two systems
JavaScript
// CommonJS (.cjs, or .js with no type:module)
const fs = require("node:fs");
module.exports = { fn };

// ES Modules (.mjs, or "type":"module" in package.json)
import fs from "node:fs";
export { fn };
const data = await import("./x.js");  // dynamic
Prefix core modules with node: (node:fs, node:path) — it's unambiguous and can't be shadowed by a malicious npm package of the same name.
Ch. 03
The Event Loop
The phases, and where your callbacks actually run.
3.1 — Phases and microtasks

Each loop tick passes through phases: timers (setTimeout), pending callbacks, poll (I/O), check (setImmediate), close. Between every phase Node drains the microtask queue: process.nextTick first, then resolved Promises.

JavaScript
setTimeout(() => log("timeout"), 0);
setImmediate(() => log("immediate"));
Promise.resolve().then(() => log("promise"));
process.nextTick(() => log("nextTick"));
// nextTick → promise → (timeout|immediate, order varies)
A recursive process.nextTick can starve the loop entirely — microtasks run to exhaustion before I/O. Prefer setImmediate for "yield then continue".
Ch. 04
Events & Emitters
The pattern most of Node's own API is built on.
4.1 — EventEmitter
JavaScript
import { EventEmitter } from "node:events";
class Bus extends EventEmitter {}
const bus = new Bus();
bus.on("job", (j) => handle(j));
bus.once("ready", init);
bus.emit("job", { id: 1 });
An "error" event with no listener throws and crashes the process. Always attach an error listener to emitters and streams.
Ch. 05
Async Patterns
Callbacks, promises, async/await, and parallelism.
5.1 — Promisify and parallelise
JavaScript
import { setTimeout as sleep } from "node:timers/promises";
import { readFile } from "node:fs/promises";

// parallel — start all, then await
const [a, b] = await Promise.all([
  readFile("a.txt"), readFile("b.txt"),
]);
// tolerate partial failure
const res = await Promise.allSettled(tasks);
Sequential await in a loop is the #1 Node performance bug. If the iterations are independent, build the promise array and Promise.all it (bound concurrency with a pool if N is large).
Ch. 06
Streams
Process data larger than memory — and respect backpressure.
6.1 — pipeline is the only safe way to chain
JavaScript
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";

await pipeline(
  createReadStream("big.log"),
  createGzip(),
  createWriteStream("big.log.gz"),
);  // handles backpressure + cleanup on error
Never chain streams with .pipe() in production — it doesn't forward errors and leaks file descriptors on failure. pipeline() does both correctly.
Ch. 07
Buffers
Raw binary — fixed-length, outside the V8 heap.
7.1 — Creating and converting
JavaScript
Buffer.from("hello", "utf8");
Buffer.alloc(16);              // zero-filled (safe)
// Buffer.allocUnsafe is faster but uninitialised — never expose
buf.toString("hex");
Buffer.concat([a, b]);
Ch. 08
File System
The promises API, and streaming vs reading whole.
8.1 — fs/promises by default
JavaScript
import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";

await mkdir("out", { recursive: true });
const txt = await readFile("a.txt", "utf8");
for (const name of await readdir(".")) log(name);
Read whole only when the file is small and bounded. For anything user-sized or unbounded, stream it (Ch. 06) — a single 2 GB readFile can OOM the process.
Ch. 09
Path & OS
Cross-platform paths and environment introspection.
9.1 — Never concatenate paths by hand
JavaScript
import path from "node:path";
import os from "node:os";

path.join(__dirname, "data", "f.json");  // OS-correct sep
path.resolve("./rel");
import.meta.dirname;   // ESM equiv of __dirname (20.11+)
os.cpus().length; os.totalmem();
Ch. 10
HTTP Server
The core http module — what every framework wraps.
10.1 — A minimal server
JavaScript
import { createServer } from "node:http";

const server = createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "content-type": "application/json" });
    return res.end(JSON.stringify({ ok: true }));
  }
  res.writeHead(404).end();
});
server.listen(3000);
Ch. 11
HTTP Client
Global fetch, timeouts, and not leaking sockets.
11.1 — fetch with a timeout
JavaScript
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), 5000);
try {
  const r = await fetch(url, { signal: ac.signal });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return await r.json();
} finally { clearTimeout(t); }
fetch has no default timeout. A hung upstream will hold the request forever and exhaust your connection pool — always wire an AbortController deadline.
Ch. 12
Error Handling
Operational vs programmer errors, and the two events that crash you.
12.1 — Crash on the unrecoverable, handle the expected
JavaScript
process.on("unhandledRejection", (err) => {
  logFatal(err);
  process.exit(1);   // let the supervisor restart a clean process
});
process.on("uncaughtException", (err) => { logFatal(err); process.exit(1); });
Don't try to "recover" from an uncaughtException and keep serving — the process is in an unknown state. Log, flush, exit; let your process manager (systemd, PM2, k8s) restart it fresh.
Ch. 13
process & env
Arguments, environment, signals, graceful shutdown.
13.1 — Graceful shutdown
JavaScript
const port = process.env.PORT ?? 3000;

function shutdown() {
  server.close(() => { db.end(); process.exit(0); });
  setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", shutdown);  // what k8s/docker send
process.on("SIGINT", shutdown);   // Ctrl-C
Ch. 14
Child Processes
Shell out, spawn long-running tools, stream their output.
14.1 — spawn (stream) vs execFile
JavaScript
import { spawn } from "node:child_process";
const p = spawn("ffmpeg", ["-i", src, out]);
p.stdout.on("data", d => log(d.toString()));
p.on("exit", code => log("done", code));
Never pass user input to exec (it runs a shell → command injection). Use spawn/execFile with an args array — no shell, no injection.
Ch. 15
Worker Threads
Real parallelism for CPU-bound work.
15.1 — Offload the loop-killer
JavaScript
import { Worker } from "node:worker_threads";

function heavy(data) {
  return new Promise((res, rej) => {
    const w = new Worker("./crunch.js", { workerData: data });
    w.on("message", res); w.on("error", rej);
  });
}
Workers are for CPU work (parsing, hashing, image processing), not I/O — I/O is already non-blocking. Pool workers; spawning one per request costs more than it saves.
Ch. 16
Cluster & Scaling
Use every core; scale out, not up.
16.1 — One process per core

A single Node process uses one core. To use a machine's N cores, run N processes behind a load balancer — via the cluster module, a process manager (PM2), or (best in modern infra) N container replicas fronted by your orchestrator. Keep processes stateless; push session/cache to Redis.

In containers, prefer "one process per container, many replicas" over the cluster module — the orchestrator already handles restart, health and balancing, and per-container observability is cleaner.
Ch. 17
npm & package.json
Scripts, lockfiles, and dependency hygiene.
17.1 — The fields that matter
json
{
  "type": "module",
  "engines": { "node": ">=20" },
  "scripts": { "start": "node app.js", "test": "node --test" }
}
Commit the lockfile and use npm ci (not npm install) in CI/CD — ci installs exactly the lockfile, deterministically, and fails if package.json and the lock disagree.
Ch. 18
Testing
The built-in test runner — zero dependencies.
18.1 — node:test
JavaScript
import { test } from "node:test";
import assert from "node:assert/strict";

test("adds", () => {
  assert.equal(add(2, 3), 5);
});
// run:  node --test
Ch. 19
Debugging
Inspector, heap snapshots, finding the leak.
19.1 — The Chrome inspector + diagnostics
Shell
$ node --inspect app.js     # chrome://inspect
$ node --prof app.js        # V8 profile → isolate-*.log
$ node --heap-prof app.js   # heap profile for leaks
A steadily climbing RSS that never drops after GC is a leak. Common causes: an ever-growing module-level Map/array used as a cache with no eviction, or listeners added without removeListener.
Ch. 20
Production Hardening
The checklist between 'it runs' and 'it stays up'.
20.1 — Non-negotiables
  • Run behind a process supervisor that restarts on crash (systemd / k8s / PM2)
  • Handle SIGTERM for graceful shutdown — drain connections before exit
  • Every outbound call has a timeout and a retry budget
  • Structured JSON logs to stdout; never log secrets or PII
  • A real /health (process up) and /ready (deps reachable) endpoint
  • Pin Node via engines and the base image tag; rebuild on security releases
  • Run as a non-root user; drop capabilities in the container
REF
Ops Cheatsheet
The flags and patterns you reach for under pressure.
Runtime flags
FlagUse
--watchauto-restart on file change (dev)
--env-file=.envload env without dotenv (20+)
--testrun the built-in test runner
--inspectattach Chrome DevTools
--profV8 CPU profile
--max-old-space-size=Nraise the heap limit (MB)