Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Node.js — User Guide

Generated May 2026 from nodejs/node at 978a8c4

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Node.js is a JavaScript runtime built on Chrome's V8 engine that executes JavaScript outside the browser, enabling server-side applications, command-line tools, and general-purpose scripting. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for data-intensive real-time applications. With over 12,000 source files and a comprehensive standard library, Node.js provides everything needed to build production systems without third-party dependencies for core functionality.


1. Quick Start

1.1 What You Get with Node.js

  • Full-featured JavaScript runtime — run JavaScript anywhere, not just in browsers
  • Comprehensive standard library — HTTP/HTTPS servers, file system, crypto, streams, DNS, and 30+ built-in modules
  • Two module systems — CommonJS (require) and ES Modules (import) with full interoperability
  • Built-in test runner — node:test module with assertions, mocking, coverage, and reporters
  • TypeScript support — native type stripping via --experimental-strip-types (powered by Amaro)
  • Permission model — fine-grained control over file system, child process, and worker access
  • Watch mode — automatic restart on file changes with --watch
  • Modern web APIs — fetch(), WebSocket, Blob, FormData, structuredClone, and more built in

Tip

If you already have Node.js installed, jump straight to the HTTP server example below. For version management across projects, install nvm or fnm first — it saves significant headaches down the road.

1.2 Prerequisites

Requirement Minimum Recommended
Operating system macOS 11+, Windows 10+, Linux (glibc 2.28+) Latest stable release
Disk space 100 MB 500 MB (with npm cache)
RAM 512 MB 2 GB+ for development
Architecture x64, ARM64 x64 or Apple Silicon

1.3 Install in One Command

macOS / Linux:

curl -fsSL https://fnm.vercel.app/install | bash && fnm install --lts

Windows (PowerShell):

winget install Schniz.fnm
fnm install --lts

1.4 Hello World

Create hello.js:

console.log("Hello from Node.js", process.version);

Run it:

node hello.js

Expected output:

Hello from Node.js v22.14.0

1.5 Hello World HTTP Server

Create server.js:

const http = require("node:http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, World!\n");
});

server.listen(3000, "127.0.0.1", () => {
  console.log("Server running at http://127.0.0.1:3000/");
});

Run it:

node server.js

Expected output:

Server running at http://127.0.0.1:3000/

Test it:

curl http://127.0.0.1:3000/
Hello, World!

Tip

During development, use node --watch server.js to automatically restart the server when you edit the file. No external tools like nodemon required.


2. Installation

2.1 System Requirements

Platform Architectures Notes
macOS 11+ (Big Sur) x64, ARM64 (Apple Silicon) Xcode Command Line Tools recommended for native addons
Windows 10+ x64, ARM64 Visual Studio Build Tools for native addons
Ubuntu 20.04+ x64, ARM64, ARMv7 build-essential for native addons
Debian 10+ x64, ARM64 build-essential for native addons
RHEL / CentOS 8+ x64, ARM64 gcc-c++ and make for native addons
Alpine 3.15+ x64, ARM64 Uses musl libc; some packages need recompilation
Docker Any host Official images at docker.io/library/node

2.2 Platform-Specific Installation

macOS

Homebrew (recommended):

brew install node

Official installer:

Download the .pkg from nodejs.org and run it.

Verify:

node --version
npm --version
v22.14.0
10.9.2

Linux

Ubuntu / Debian:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

RHEL / Fedora / CentOS:

curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs

Alpine:

apk add nodejs npm

Important

Distribution-provided Node.js packages are often severely outdated. Use NodeSource, fnm, or nvm to get current versions. Running an unsupported Node.js version means no security patches.

Windows

winget (Windows 10+):

winget install OpenJS.NodeJS.LTS

Chocolatey:

choco install nodejs-lts

Official installer:

Download the .msi from nodejs.org. The installer adds node and npm to your PATH automatically.

Warning

On Windows, building native addons (packages with C++ bindings) requires the "Desktop development with C++" workload from Visual Studio Build Tools. Install it with: npm install --global windows-build-tools or download Visual Studio Build Tools directly.

Docker

docker run -it --rm node:22-alpine node -e "console.log('Node', process.version)"

Dockerfile for production:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

2.3 Version Managers

Version managers let you switch Node.js versions per project. Two popular choices:

fnm (Fast Node Manager)

# Install fnm
curl -fsSL https://fnm.vercel.app/install | bash

# Install a specific version
fnm install 22
fnm install 20

# Use a version
fnm use 22

# Set default
fnm default 22

# Auto-switch per directory (.node-version or .nvmrc)
fnm env --use-on-cd

nvm (Node Version Manager)

# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Restart terminal, then:
nvm install --lts
nvm install 22
nvm install 20

# Switch versions
nvm use 22

# Set default
nvm alias default 22

# Use .nvmrc file
echo "22" > .nvmrc
nvm use

Create a .node-version file in your project root:

echo "22" > .node-version

Both fnm and nvm will detect this file and switch automatically (with shell integration enabled).

2.4 Building from Source

Building from source is necessary when targeting unsupported platforms, enabling custom build flags, or debugging Node.js internals.

Prerequisites:

Dependency Minimum Version
Python 3.12+
GCC / Clang GCC 10.1+ or Clang 8+
GNU Make 3.81+
Git any recent

Build steps:

git clone https://github.com/nodejs/node.git
cd node
git checkout v22.14.0

# Configure (see below for flags)
./configure

# Build (use -j for parallel compilation)
make -j$(nproc)

# Test (optional)
make test

# Install
sudo make install

Common configure.py flags:

Flag Purpose
--prefix=/usr/local Installation directory
--debug Build with debug symbols
--without-npm Skip bundling npm
--shared-openssl Link against system OpenSSL
--shared-zlib Link against system zlib
--shared-libuv Link against system libuv
--with-intl=full-icu Full internationalization support
--with-intl=small-icu English-only ICU (smaller binary)
--without-intl No internationalization
--openssl-use-def-ca-store Use system certificate store
--ninja Use Ninja build system (faster)
--enable-lto Enable Link-Time Optimization
--fully-static Fully static binary
--without-inspector Disable inspector/debugger protocol
--without-dtrace Disable DTrace probes
--without-etw Disable Event Tracing for Windows
--v8-lite-mode V8 lite mode (less memory, slower JIT)
--without-node-options Disable NODE_OPTIONS env var
--without-ssl Build without TLS support
--with-arm-float-abi ARM floating point ABI (hard/soft/softfp)
--with-arm-fpu ARM FPU type (vfp, vfpv3, neon)
--cross-compiling Enable cross-compilation
--dest-cpu Target CPU architecture
--dest-os Target operating system

Note

The full list of configure flags is available via ./configure --help. For architecture details, security hardening options, and advanced build configurations, see the Admin Guide.

2.5 Post-Install Verification

Run these commands to confirm everything works:

# Check versions
node --version
npm --version

# Test JavaScript execution
node -e "console.log('Node.js works!')"

# Test module loading
node -e "const os = require('node:os'); console.log(os.platform(), os.arch())"

# Test npm
npm --version
npm init -y

Expected output pattern:

v22.14.0
10.9.2
Node.js works!
linux x64

2.6 Installation Troubleshooting

node: command not found

Cause: Node.js is not in your system PATH.

Solution:

  1. If installed via nvm/fnm: run nvm use default or fnm use default
  2. If installed from package: check /usr/local/bin/node exists
  3. Add to PATH manually: export PATH="/usr/local/bin:$PATH" in your shell profile

npm ERR! EACCES permission denied

Cause: npm is trying to write to a global directory owned by root.

Solution:

  1. Use a version manager (nvm/fnm) — this avoids the problem entirely
  2. Or configure npm to use your home directory: npm config set prefix '~/.npm-global' and add ~/.npm-global/bin to PATH

gyp ERR! stack Error: Could not find any Python installation

Cause: Native addon compilation requires Python.

Solution:

# macOS
xcode-select --install

# Ubuntu/Debian
sudo apt install python3 build-essential

# Windows
npm install --global windows-build-tools

Error: Cannot find module 'node:...'

Cause: You are running a Node.js version too old for the node: prefix (added in v16.0.0).

Solution: Upgrade to Node.js 16+ or remove the node: prefix from imports.

N-API version mismatch

Cause: Native addon was compiled for a different Node.js version.

Solution:

npm rebuild
# or
rm -rf node_modules && npm install

Error: ENOSPC: System limit for number of file watchers reached

Cause: Linux inotify watcher limit is too low (common with large projects or --watch mode).

Solution:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

GLIBC_2.28 not found

Cause: Your Linux distribution has an older glibc than Node.js requires.

Solution: Upgrade your distribution or use the Alpine/musl build, or build Node.js from source.

ERR_OSSL_EVP_UNSUPPORTED

Cause: OpenSSL 3.0 dropped support for some legacy algorithms. Common when upgrading to Node.js 17+.

Solution:

# Temporary workaround (not recommended for production)
node --openssl-legacy-provider your-script.js

# Permanent fix: update the affected package
npm update

Digital envelope routines::unsupported

Cause: Same root cause as above — OpenSSL 3.0 incompatibility with older packages using MD4/MD5 for hashing.

Solution:

export NODE_OPTIONS=--openssl-legacy-provider

MODULE_NOT_FOUND after nvm use

Cause: Global packages were installed under a different Node.js version.

Solution:

nvm install <version> --reinstall-packages-from=<old-version>

Multiple Node.js installations conflicting

Cause: System package manager and version manager both installed Node.js.

Solution:

# Find all node binaries
which -a node

# Remove system version
sudo apt remove nodejs  # Debian/Ubuntu
brew uninstall node     # macOS

# Use only the version manager
nvm use default

3. Configuration

3.1 Environment Variables

Node.js behavior is controlled through environment variables. These are the most commonly used:

Variable Purpose Example
NODE_ENV Application environment mode production, development, test
NODE_OPTIONS Pass CLI flags via environment --max-old-space-size=4096
NODE_PATH Additional module search paths /usr/local/lib/node_modules
NODE_DEBUG Enable debug output for core modules http,net,tls
NODE_TLS_REJECT_UNAUTHORIZED Disable TLS certificate validation 0 (insecure, dev only)
NODE_ICU_DATA Path to custom ICU data file /usr/share/icu/icudt72l.dat
UV_THREADPOOL_SIZE libuv thread pool size (default: 4) 8 (max: 1024)
NODE_NO_WARNINGS Suppress all process warnings 1
NODE_PENDING_DEPRECATION Emit pending deprecation warnings 1
NODE_DISABLE_COLORS Disable ANSI colors in stdout 1
NO_COLOR Disable colors (standard, all tools) 1
FORCE_COLOR Force color output even in non-TTY 1, 2, or 3 (color depth)
NODE_EXTRA_CA_CERTS Path to additional CA certificates PEM file /etc/ssl/custom-ca.pem
NODE_REDIRECT_WARNINGS Write warnings to a file instead of stderr /var/log/node-warnings.log
TZ Timezone for Date and Intl America/New_York

Warning

Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification globally. Never do this in production — it makes your application vulnerable to man-in-the-middle attacks. Use NODE_EXTRA_CA_CERTS to add custom CA certificates instead.

Setting environment variables:

# Inline for one command
NODE_ENV=production node server.js

# Export for the session
export NODE_ENV=production
export UV_THREADPOOL_SIZE=8
node server.js

# In package.json scripts
{
  "scripts": {
    "start": "NODE_ENV=production node server.js",
    "dev": "NODE_ENV=development node --watch server.js"
  }
}

3.2 The .env File Support

Node.js has built-in .env file loading via the --env-file flag:

node --env-file=.env server.js

.env file format:

# Comments start with #
NODE_ENV=production
DATABASE_URL=postgres://localhost:5432/mydb
API_KEY=sk-abc123
PORT=3000

# Multi-line values use quotes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"

You can load multiple .env files (later files override earlier):

node --env-file=.env --env-file=.env.local server.js

Note

The --env-file flag loads variables into process.env before your code runs. It does not override existing environment variables — system-set variables take precedence.

3.3 Package.json Configuration

The package.json file controls project metadata, dependencies, and module resolution:

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "main": "./src/index.js",
  "exports": {
    ".": "./src/index.js",
    "./utils": "./src/utils.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "node --test"
  }
}

Key fields:

Field Purpose
"type": "module" Treat .js files as ES Modules (import/export syntax)
"type": "commonjs" Treat .js files as CommonJS (require syntax) — this is the default
"main" Entry point for require('package-name')
"exports" Modern entry point mapping (takes precedence over "main")
"imports" Private import maps (subpath imports starting with #)
"engines" Declare required Node.js/npm versions

3.4 TypeScript Configuration

Node.js supports TypeScript via --experimental-strip-types, which removes type annotations at load time (powered by Amaro/SWC):

node --experimental-strip-types app.ts

For projects using TypeScript, create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"]
}

Important

The --experimental-strip-types flag only removes type annotations — it does not support TypeScript features that require code transformation, such as enum, namespace, or legacy decorators. Use tsc or a bundler for those features.

3.5 NODE_OPTIONS

NODE_OPTIONS lets you set CLI flags via environment variable, useful for production deployments where you cannot modify the node command:

# Increase heap memory
export NODE_OPTIONS="--max-old-space-size=4096"

# Enable source maps
export NODE_OPTIONS="--enable-source-maps"

# Combine multiple flags
export NODE_OPTIONS="--max-old-space-size=4096 --enable-source-maps"

Not all flags are allowed in NODE_OPTIONS for security reasons. Flags like --eval, --print, and --input-type are disallowed.

For complete configuration reference, runtime tuning, V8 flags, and memory management, see the Admin Guide.


4. Core Usage

4.1 Running JavaScript

Basic Execution

# Run a file
node app.js

# Evaluate code inline
node --eval "console.log(2 + 2)"
# Output: 4

# Evaluate and print the result (no console.log needed)
node --print "Math.PI * 2"
# Output: 6.283185307179586

# Read from stdin
echo 'console.log("from stdin")' | node
# Output: from stdin

The REPL

Start an interactive Read-Eval-Print Loop:

node
Welcome to Node.js v22.14.0.
Type ".help" for more information.
> 2 + 2
4
> const fs = require('node:fs')
undefined
> fs.readdirSync('.').length
12
> .exit

REPL special commands:

Command Action
.help Show available commands
.break Abort multi-line input
.clear Reset context
.editor Enter multi-line editor mode
.exit Exit the REPL
.load filename Load a file into the session
.save filename Save the session to a file

Tab completion works for globals, modules, and object properties. Press Tab twice to see all completions.

Watch Mode

Automatically restart when files change:

# Watch the entry file and its dependencies
node --watch app.js

# Watch specific paths
node --watch-path=./src --watch-path=./config app.js

# Watch mode with inspect
node --watch --inspect app.js
[watch] restarting due to changes...
[watch] restarted successfully

Tip

Watch mode is built into Node.js since v18.11. It monitors the entry file and all transitively imported/required files. Use --watch-path to include configuration files or directories not directly imported.

4.2 Module Systems

Node.js supports two module systems: CommonJS (the original) and ES Modules (the standard).

CommonJS (CJS)

// math.js — exporting
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };
// app.js — importing
const { add, multiply } = require("./math.js");

console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20

CommonJS characteristics:

  • require() is synchronous
  • module.exports defines what a module exposes
  • Files are wrapped in a function: (function(exports, require, module, __filename, __dirname) { ... })
  • Circular dependencies are handled (partial exports returned)

ES Modules (ESM)

// math.mjs — exporting
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export default { add, multiply };
// app.mjs — importing
import { add, multiply } from "./math.mjs";
import math from "./math.mjs";

console.log(add(2, 3));          // 5
console.log(math.multiply(4, 5)); // 20

How Node.js decides which system to use:

Condition Module System
.mjs extension Always ES Modules
.cjs extension Always CommonJS
.js extension + "type": "module" in nearest package.json ES Modules
.js extension + "type": "commonjs" or no "type" in nearest package.json CommonJS

Important

ES Modules are asynchronous and have different scoping rules. require(), __filename, __dirname, module, and exports are NOT available in ES Modules. Use import.meta.url and import.meta.dirname (v21.2+) instead.

ES Module equivalents for CommonJS globals:

// __filename equivalent
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);

// __dirname equivalent
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));

// Or in Node.js 21.2+:
const __dirname = import.meta.dirname;
const __filename = import.meta.filename;

// require() equivalent for JSON
import { readFileSync } from "node:fs";
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));

// Or with import attributes (experimental)
import pkg from "./package.json" with { type: "json" };

Importing CommonJS from ESM and Vice Versa

// ESM can import CommonJS
import cjsModule from "./legacy.cjs";

// CommonJS can import ESM (async)
async function main() {
  const esmModule = await import("./modern.mjs");
}
main();

4.3 File System Operations

The node:fs module provides file system access. It offers three API styles: callback, synchronous, and promise-based.

Reading Files

import { readFile, readFileSync } from "node:fs";
import { readFile as readFileAsync } from "node:fs/promises";

// Callback style
readFile("data.txt", "utf-8", (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Synchronous (blocks the event loop)
const content = readFileSync("data.txt", "utf-8");
console.log(content);

// Promise style (recommended)
const data = await readFileAsync("data.txt", "utf-8");
console.log(data);

Writing Files

import { writeFile, appendFile } from "node:fs/promises";

// Write (creates or overwrites)
await writeFile("output.txt", "Hello, World!\n", "utf-8");

// Append
await appendFile("log.txt", `${new Date().toISOString()} Event occurred\n`);

// Write with options
await writeFile("config.json", JSON.stringify(config, null, 2), {
  encoding: "utf-8",
  mode: 0o644,
  flag: "wx", // fail if file exists
});

Directory Operations

import { readdir, mkdir, rm, stat } from "node:fs/promises";

// List directory contents
const files = await readdir("./src");
console.log(files); // ['index.js', 'utils.js', 'lib']

// List with file types
const entries = await readdir("./src", { withFileTypes: true });
for (const entry of entries) {
  console.log(`${entry.name} - ${entry.isDirectory() ? "dir" : "file"}`);
}

// Recursive directory listing
const allFiles = await readdir("./src", { recursive: true });

// Create directory (recursive)
await mkdir("./data/logs/2026", { recursive: true });

// Remove directory (recursive)
await rm("./temp", { recursive: true, force: true });

// File/directory info
const info = await stat("./package.json");
console.log(`Size: ${info.size} bytes`);
console.log(`Modified: ${info.mtime}`);
console.log(`Is file: ${info.isFile()}`);

Watching for File Changes

import { watch } from "node:fs/promises";

const watcher = watch("./src", { recursive: true });

for await (const event of watcher) {
  console.log(`${event.eventType}: ${event.filename}`);
}

Note

The fs.watch() API is platform-dependent. On Linux, it uses inotify; on macOS, FSEvents; on Windows, ReadDirectoryChangesW. Behavior for recursive watching varies — it is fully supported on macOS and Windows, and on Linux since Node.js 19.

File Operations

import { copyFile, rename, access, constants } from "node:fs/promises";

// Copy a file
await copyFile("source.txt", "dest.txt");

// Rename / move
await rename("old.txt", "new.txt");

// Check if file exists and is readable
try {
  await access("config.json", constants.R_OK);
  console.log("File is readable");
} catch {
  console.log("File is not readable or does not exist");
}

4.4 HTTP/HTTPS Servers

Basic HTTP Server

import http from "node:http";

const server = http.createServer((req, res) => {
  const { method, url, headers } = req;

  if (method === "GET" && url === "/") {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("<h1>Welcome</h1>");
    return;
  }

  if (method === "GET" && url === "/api/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
    return;
  }

  if (method === "POST" && url === "/api/data") {
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      const data = JSON.parse(body);
      res.writeHead(201, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ received: data }));
    });
    return;
  }

  res.writeHead(404, { "Content-Type": "text/plain" });
  res.end("Not Found");
});

server.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});

HTTPS Server

import https from "node:https";
import { readFileSync } from "node:fs";

const options = {
  key: readFileSync("./certs/server.key"),
  cert: readFileSync("./certs/server.cert"),
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end("Secure connection established\n");
});

server.listen(443, () => {
  console.log("HTTPS server running on port 443");
});

Making HTTP Requests

Using the global fetch (recommended):

// GET request
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);

// POST request
const result = await fetch("https://api.example.com/submit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Node.js", version: 22 }),
});
console.log(result.status); // 201

Using http.get (lower level):

import http from "node:http";

http.get("http://example.com", (res) => {
  let data = "";
  res.on("data", (chunk) => (data += chunk));
  res.on("end", () => console.log(data));
});

4.5 Streams

Streams are fundamental to Node.js — they let you process data piece by piece without loading it all into memory.

Stream Types

Type Description Example
Readable Source of data fs.createReadStream(), http.IncomingMessage
Writable Destination for data fs.createWriteStream(), http.ServerResponse
Duplex Both readable and writable net.Socket, zlib streams
Transform Modify data as it passes through zlib.createGzip(), crypto.createCipheriv()

Reading a Large File with Streams

import { createReadStream } from "node:fs";

const stream = createReadStream("large-file.log", { encoding: "utf-8" });

let lineCount = 0;
let remainder = "";

stream.on("data", (chunk) => {
  const lines = (remainder + chunk).split("\n");
  remainder = lines.pop(); // last partial line
  lineCount += lines.length;
});

stream.on("end", () => {
  if (remainder) lineCount++;
  console.log(`Total lines: ${lineCount}`);
});
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";

// Compress a file
await pipeline(
  createReadStream("input.txt"),
  createGzip(),
  createWriteStream("input.txt.gz")
);

console.log("Compression complete");

Caution

Never pipe streams manually with .pipe() in production without error handling. Use stream.pipeline() instead — it properly handles errors and cleans up resources (destroys streams) when any stream in the chain fails or the destination closes prematurely.

Custom Transform Stream

import { Transform } from "node:stream";

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

process.stdin.pipe(upperCase).pipe(process.stdout);

4.6 Child Processes

The node:child_process module lets you run shell commands, other programs, and Node.js scripts as separate processes.

import { exec, execFile, spawn, fork } from "node:child_process";
import { execSync } from "node:child_process";

exec — Run a Shell Command

import { exec } from "node:child_process";

exec("ls -la", (error, stdout, stderr) => {
  if (error) {
    console.error(`Error: ${error.message}`);
    return;
  }
  console.log(stdout);
});

// Promise version
import { exec as execCb } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(execCb);

const { stdout } = await execAsync("git log --oneline -5");
console.log(stdout);

spawn — Stream Output from a Long-Running Process

import { spawn } from "node:child_process";

const child = spawn("find", [".", "-name", "*.js"]);

child.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
});

child.stderr.on("data", (data) => {
  console.error(`stderr: ${data}`);
});

child.on("close", (code) => {
  console.log(`Process exited with code ${code}`);
});

fork — Run a Node.js Script with IPC

// parent.js
import { fork } from "node:child_process";

const child = fork("./worker.js");

child.on("message", (msg) => {
  console.log("From child:", msg);
});

child.send({ task: "compute", data: [1, 2, 3, 4, 5] });
// worker.js
process.on("message", (msg) => {
  if (msg.task === "compute") {
    const result = msg.data.reduce((a, b) => a + b, 0);
    process.send({ result });
  }
});

execFile — Run a Binary Directly (No Shell)

import { execFile } from "node:child_process";

execFile("python3", ["--version"], (error, stdout) => {
  console.log(stdout.trim()); // Python 3.12.0
});

Synchronous Execution

import { execSync, spawnSync } from "node:child_process";

// Quick shell command
const output = execSync("whoami", { encoding: "utf-8" });
console.log(output.trim());

// With error handling
try {
  execSync("false");
} catch (error) {
  console.log(`Command failed with code ${error.status}`);
}

4.7 Worker Threads

Worker threads enable true parallelism for CPU-intensive tasks without the overhead of separate processes.

// main.js
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";

if (isMainThread) {
  // Main thread — create workers
  const worker = new Worker(new URL(import.meta.url), {
    workerData: { start: 0, end: 1_000_000 },
  });

  worker.on("message", (result) => {
    console.log(`Sum: ${result}`);
  });

  worker.on("error", (err) => {
    console.error("Worker error:", err);
  });

  worker.on("exit", (code) => {
    if (code !== 0) console.error(`Worker exited with code ${code}`);
  });
} else {
  // Worker thread — do computation
  const { start, end } = workerData;
  let sum = 0;
  for (let i = start; i < end; i++) {
    sum += i;
  }
  parentPort.postMessage(sum);
}

Worker pool pattern:

import { Worker } from "node:worker_threads";
import { cpus } from "node:os";

class WorkerPool {
  #workers = [];
  #queue = [];

  constructor(workerFile, poolSize = cpus().length) {
    for (let i = 0; i < poolSize; i++) {
      this.#addWorker(workerFile);
    }
  }

  #addWorker(workerFile) {
    const worker = new Worker(workerFile);
    worker.busy = false;
    worker.on("message", (result) => {
      worker.busy = false;
      worker.currentResolve(result);
      this.#processQueue();
    });
    this.#workers.push(worker);
  }

  #processQueue() {
    if (this.#queue.length === 0) return;
    const freeWorker = this.#workers.find((w) => !w.busy);
    if (!freeWorker) return;
    const { data, resolve } = this.#queue.shift();
    freeWorker.busy = true;
    freeWorker.currentResolve = resolve;
    freeWorker.postMessage(data);
  }

  run(data) {
    return new Promise((resolve) => {
      const freeWorker = this.#workers.find((w) => !w.busy);
      if (freeWorker) {
        freeWorker.busy = true;
        freeWorker.currentResolve = resolve;
        freeWorker.postMessage(data);
      } else {
        this.#queue.push({ data, resolve });
      }
    });
  }
}

Tip

Use worker threads for CPU-bound tasks (image processing, compression, heavy math). For I/O-bound tasks (HTTP requests, file reads, database queries), async/await with the event loop is more efficient and simpler.

4.8 Events and EventEmitter

import { EventEmitter } from "node:events";

class OrderSystem extends EventEmitter {
  placeOrder(order) {
    // Process order...
    this.emit("order:placed", order);
  }

  cancelOrder(orderId) {
    // Cancel order...
    this.emit("order:cancelled", { orderId });
  }
}

const orders = new OrderSystem();

orders.on("order:placed", (order) => {
  console.log(`New order: ${order.id}`);
});

orders.on("order:placed", (order) => {
  // Send notification email...
});

orders.once("order:cancelled", (info) => {
  console.log(`First cancellation: ${info.orderId}`);
});

// Async event handling
import { once } from "node:events";

const [order] = await once(orders, "order:placed");
console.log("Got order:", order);

Key methods:

Method Description
emitter.on(event, listener) Add a persistent listener
emitter.once(event, listener) Add a one-time listener
emitter.off(event, listener) Remove a listener
emitter.emit(event, ...args) Emit an event
emitter.removeAllListeners(event?) Remove all listeners
emitter.listenerCount(event) Count listeners for an event
emitter.setMaxListeners(n) Set max listener warning threshold (default: 10)
events.once(emitter, event) Promise-based one-time listener

4.9 Buffers and Binary Data

Buffers represent fixed-length raw binary data. Essential for file I/O, network protocols, and binary formats.

import { Buffer } from "node:buffer";

// Create buffers
const buf1 = Buffer.alloc(10);          // 10 zero-filled bytes
const buf2 = Buffer.from("Hello");      // From string (UTF-8)
const buf3 = Buffer.from([0x48, 0x69]); // From byte array
const buf4 = Buffer.from("SGVsbG8=", "base64"); // From base64

// Read buffer content
console.log(buf2.toString());          // "Hello"
console.log(buf2.toString("hex"));     // "48656c6c6f"
console.log(buf2.toString("base64"));  // "SGVsbG8="
console.log(buf3.toString());          // "Hi"

// Buffer operations
console.log(buf2.length);              // 5
console.log(buf2[0]);                  // 72 (ASCII 'H')
console.log(buf2.includes("ell"));     // true
console.log(Buffer.compare(buf1, buf2)); // -1, 0, or 1

// Concatenate
const combined = Buffer.concat([buf2, Buffer.from(" "), buf3]);
console.log(combined.toString()); // "Hello Hi"

// Slice (shares memory!)
const slice = buf2.subarray(0, 3);
console.log(slice.toString()); // "Hel"

4.10 URL and Query String Parsing

import { URL, URLSearchParams } from "node:url";

// Parse a URL
const url = new URL("https://example.com:8080/path/to/page?name=Node&v=22#section");

console.log(url.hostname);    // "example.com"
console.log(url.port);        // "8080"
console.log(url.pathname);    // "/path/to/page"
console.log(url.search);      // "?name=Node&v=22"
console.log(url.hash);        // "#section"
console.log(url.origin);      // "https://example.com:8080"

// Work with query parameters
const params = url.searchParams;
console.log(params.get("name"));   // "Node"
console.log(params.has("v"));      // true
params.append("lang", "en");
console.log(url.toString());
// https://example.com:8080/path/to/page?name=Node&v=22&lang=en#section

// Build a URL
const apiUrl = new URL("/api/v1/users", "https://example.com");
apiUrl.searchParams.set("page", "1");
apiUrl.searchParams.set("limit", "50");
console.log(apiUrl.toString());
// https://example.com/api/v1/users?page=1&limit=50

4.11 Path Manipulation

import path from "node:path";

// Join path segments safely
const fullPath = path.join("/home", "user", "documents", "file.txt");
// Linux: /home/user/documents/file.txt
// Windows: \home\user\documents\file.txt

// Resolve to absolute path
const abs = path.resolve("src", "utils", "index.js");
// e.g., /Users/dev/project/src/utils/index.js

// Parse path components
const parsed = path.parse("/home/user/file.txt");
console.log(parsed);
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

// Common operations
console.log(path.basename("/path/to/file.js"));      // "file.js"
console.log(path.basename("/path/to/file.js", ".js")); // "file"
console.log(path.dirname("/path/to/file.js"));        // "/path/to"
console.log(path.extname("archive.tar.gz"));          // ".gz"
console.log(path.relative("/a/b/c", "/a/d/e"));       // "../../d/e"
console.log(path.isAbsolute("/home/user"));            // true
console.log(path.isAbsolute("./relative"));            // false

// Platform-specific separator
console.log(path.sep); // "/" on POSIX, "\\" on Windows

// Normalize messy paths
console.log(path.normalize("/foo/bar//baz/./quux/..")); // "/foo/bar/baz"

Warning

Always use path.join() or path.resolve() instead of string concatenation for paths. Concatenation with "/" breaks on Windows; path.join() uses the correct separator automatically.

4.12 Crypto Operations

The node:crypto module provides cryptographic functionality including hashing, encryption, signing, and random number generation.

Hashing

import { createHash, randomBytes, randomUUID } from "node:crypto";

// SHA-256 hash
const hash = createHash("sha256")
  .update("Hello, World!")
  .digest("hex");
console.log(hash);
// dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f

// File hash
import { createReadStream } from "node:fs";

async function hashFile(filepath) {
  const hash = createHash("sha256");
  const stream = createReadStream(filepath);
  for await (const chunk of stream) {
    hash.update(chunk);
  }
  return hash.digest("hex");
}

// Random values
const token = randomBytes(32).toString("hex");    // 64-char hex token
const uuid = randomUUID();                        // v4 UUID

Encryption and Decryption

import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";

const algorithm = "aes-256-gcm";
const password = "super-secret-password";
const salt = randomBytes(16);
const key = scryptSync(password, salt, 32);

function encrypt(plaintext) {
  const iv = randomBytes(16);
  const cipher = createCipheriv(algorithm, key, iv);

  let encrypted = cipher.update(plaintext, "utf-8", "hex");
  encrypted += cipher.final("hex");
  const authTag = cipher.getAuthTag().toString("hex");

  return { encrypted, iv: iv.toString("hex"), authTag };
}

function decrypt({ encrypted, iv, authTag }) {
  const decipher = createDecipheriv(algorithm, key, Buffer.from(iv, "hex"));
  decipher.setAuthTag(Buffer.from(authTag, "hex"));

  let decrypted = decipher.update(encrypted, "hex", "utf-8");
  decrypted += decipher.final("utf-8");
  return decrypted;
}

const encrypted = encrypt("Hello, World!");
console.log("Encrypted:", encrypted);

const decrypted = decrypt(encrypted);
console.log("Decrypted:", decrypted); // "Hello, World!"

HMAC Signing

import { createHmac } from "node:crypto";

const secret = "webhook-secret-key";
const payload = JSON.stringify({ event: "push", repo: "myrepo" });

const signature = createHmac("sha256", secret)
  .update(payload)
  .digest("hex");

console.log(`sha256=${signature}`);
// Verify: recompute and compare with timing-safe equality

4.13 DNS Resolution

import { promises as dns } from "node:dns";

// Resolve hostname to IP
const addresses = await dns.resolve4("nodejs.org");
console.log(addresses); // ['104.20.22.46', '104.20.23.46']

// Reverse lookup
const hostnames = await dns.reverse("8.8.8.8");
console.log(hostnames); // ['dns.google']

// MX records
const mx = await dns.resolveMx("gmail.com");
console.log(mx);
// [{ exchange: 'alt1.gmail-smtp-in.l.google.com', priority: 10 }, ...]

// All record types
const allRecords = await dns.resolveAny("example.com");

// Lookup (uses OS resolver, respects /etc/hosts)
const result = await dns.lookup("localhost");
console.log(result); // { address: '127.0.0.1', family: 4 }

Note

dns.lookup() uses the operating system's DNS resolution (getaddrinfo), which respects /etc/hosts and system DNS configuration. dns.resolve*() methods connect directly to DNS servers, bypassing the OS resolver. In most application code, dns.lookup() is what you want.

4.14 Timers

// setTimeout — run once after delay
const timeoutId = setTimeout(() => {
  console.log("Runs after 2 seconds");
}, 2000);

// clearTimeout — cancel
clearTimeout(timeoutId);

// setInterval — run repeatedly
const intervalId = setInterval(() => {
  console.log("Every 5 seconds:", new Date().toISOString());
}, 5000);

// Stop after 30 seconds
setTimeout(() => clearInterval(intervalId), 30000);

// setImmediate — run at end of current event loop iteration
setImmediate(() => {
  console.log("Runs after I/O callbacks");
});

// Promise-based timers (Node.js 16+)
import { setTimeout as sleep } from "node:timers/promises";

await sleep(1000);
console.log("1 second later");

// AbortController integration
const controller = new AbortController();
try {
  await sleep(60000, null, { signal: controller.signal });
} catch (err) {
  if (err.name === "AbortError") {
    console.log("Timer was cancelled");
  }
}
// Cancel from elsewhere: controller.abort();

// Scheduling timer with ref/unref
const timer = setTimeout(() => {}, 60000);
timer.unref(); // Don't keep the process alive for this timer

4.15 Built-in Test Runner

Node.js includes a test runner in the node:test module. No external testing framework required.

Writing Tests

// test/math.test.js
import { describe, it, before, after, beforeEach, mock } from "node:test";
import assert from "node:assert/strict";

import { add, multiply, divide } from "../src/math.js";

describe("Math module", () => {
  describe("add()", () => {
    it("should add two positive numbers", () => {
      assert.strictEqual(add(2, 3), 5);
    });

    it("should handle negative numbers", () => {
      assert.strictEqual(add(-1, -2), -3);
    });

    it("should handle zero", () => {
      assert.strictEqual(add(0, 5), 5);
    });
  });

  describe("divide()", () => {
    it("should divide numbers", () => {
      assert.strictEqual(divide(10, 2), 5);
    });

    it("should throw on division by zero", () => {
      assert.throws(() => divide(1, 0), {
        message: "Division by zero",
      });
    });
  });
});

Running Tests

# Run all tests in test/ directory
node --test

# Run specific test files
node --test test/math.test.js test/utils.test.js

# Run with glob pattern
node --test "test/**/*.test.js"

# Watch mode — rerun on changes
node --test --watch

# With coverage
node --test --experimental-test-coverage

# Specific reporter
node --test --test-reporter=spec
node --test --test-reporter=dot
node --test --test-reporter=tap

# Only run tests matching a name pattern
node --test --test-name-pattern="add"

# Concurrency control
node --test --test-concurrency=4

Expected output (spec reporter):

▶ Math module
  ▶ add()
    ✔ should add two positive numbers (0.5ms)
    ✔ should handle negative numbers (0.1ms)
    ✔ should handle zero (0.1ms)
  ▶ divide()
    ✔ should divide numbers (0.1ms)
    ✔ should throw on division by zero (0.2ms)

ℹ tests 5
ℹ suites 3
ℹ pass 5
ℹ fail 0
ℹ duration_ms 45

Mocking

import { describe, it, mock } from "node:test";
import assert from "node:assert/strict";

describe("Mocking", () => {
  it("should mock a function", () => {
    const fn = mock.fn((x) => x * 2);

    assert.strictEqual(fn(5), 10);
    assert.strictEqual(fn.mock.calls.length, 1);
    assert.deepStrictEqual(fn.mock.calls[0].arguments, [5]);
  });

  it("should mock timers", (t) => {
    t.mock.timers.enable({ apis: ["setTimeout"] });

    let called = false;
    setTimeout(() => { called = true; }, 5000);

    t.mock.timers.tick(5000);
    assert.strictEqual(called, true);
  });

  it("should mock a method", (t) => {
    const obj = {
      getValue() { return "real"; }
    };

    t.mock.method(obj, "getValue", () => "mocked");
    assert.strictEqual(obj.getValue(), "mocked");
  });
});

Subtests, Hooks, and Lifecycle

import { describe, it, before, after, beforeEach, afterEach } from "node:test";

describe("Database tests", () => {
  let db;

  before(async () => {
    db = await connectToTestDb();
  });

  after(async () => {
    await db.close();
  });

  beforeEach(async () => {
    await db.clear();
  });

  it("should insert a record", async () => {
    await db.insert({ name: "test" });
    const count = await db.count();
    assert.strictEqual(count, 1);
  });
});

4.16 TypeScript Support

Node.js supports TypeScript directly via the --experimental-strip-types flag, which strips type annotations at load time:

node --experimental-strip-types app.ts
// app.ts
interface User {
  name: string;
  age: number;
}

function greet(user: User): string {
  return `Hello, ${user.name}! You are ${user.age} years old.`;
}

const user: User = { name: "Alice", age: 30 };
console.log(greet(user));
Hello, Alice! You are 30 years old.

What works:

  • Type annotations (: string, : number, etc.)
  • Interfaces
  • Type aliases
  • Generics
  • as type assertions
  • Optional chaining and nullish coalescing (these are JavaScript too)

What does NOT work (requires a build step):

  • enum declarations
  • namespace declarations
  • Legacy decorators (Stage 2 decorators work)
  • const enum
  • Implicit file extensions (must use .ts)
  • import type transformation

Important

Type stripping does not perform type checking. Run tsc --noEmit separately for type validation. The --experimental-strip-types flag is for runtime convenience, not as a replacement for the TypeScript compiler.

4.17 Debugging

Inspector Protocol (Chrome DevTools)

# Start with inspector on default port 9229
node --inspect app.js

# Break on first line
node --inspect-brk app.js

# Custom port
node --inspect=0.0.0.0:9230 app.js
Debugger listening on ws://127.0.0.1:9229/6f8c1e3a-...
For help, see: https://nodejs.org/en/docs/inspector

Open chrome://inspect in Chrome, click "Open dedicated DevTools for Node", and connect.

Built-in Debugger (CLI)

node inspect app.js
< Debugger listening on ws://127.0.0.1:9229/...
connecting to 127.0.0.1:9229 ... ok
Break on start in app.js:1
> 1 const http = require('node:http');
  2
  3 const server = http.createServer((req, res) => {
debug>

Debugger commands:

Command Action
cont, c Continue
next, n Step over
step, s Step into
out, o Step out
pause Pause running code
repl Enter REPL to evaluate expressions
watch('expr') Add watch expression
setBreakpoint(line), sb(line) Set breakpoint
clearBreakpoint(file, line), cb(file, line) Clear breakpoint
backtrace, bt Print call stack
.exit Exit debugger

Programmatic Debugging

// Add debugger statement in code
function processData(data) {
  debugger; // Execution pauses here when inspector is attached
  return data.map((item) => item * 2);
}

Debug Logging with NODE_DEBUG

# Enable debug output for specific modules
NODE_DEBUG=http,net node app.js
HTTP 12345: call onSocket 0 0
HTTP 12345: createConnection example.com:80
NET 12345: connect: find host example.com
NET 12345: connect: dns options ...

4.18 Permission Model

The permission model restricts what a Node.js application can access, providing defense in depth.

# Enable the permission model (denies everything by default)
node --experimental-permission app.js

# Allow reading specific directories
node --experimental-permission --allow-fs-read=/app/data --allow-fs-read=/app/config app.js

# Allow writing to specific directory
node --experimental-permission --allow-fs-write=/app/output app.js

# Allow child process spawning
node --experimental-permission --allow-child-process app.js

# Allow worker threads
node --experimental-permission --allow-worker app.js

# Combine permissions
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/output \
  --allow-child-process \
  app.js

# Allow all reads (wildcard)
node --experimental-permission --allow-fs-read=* app.js

Permission types:

Permission Flag Controls
File system read --allow-fs-read fs.readFile, fs.readdir, fs.stat, etc.
File system write --allow-fs-write fs.writeFile, fs.mkdir, fs.unlink, etc.
Child process --allow-child-process child_process.spawn, exec, etc.
Worker threads --allow-worker new Worker()
Addons --allow-addons Loading native C++ addons
WASI --allow-wasi WebAssembly System Interface
FFI --allow-ffi Foreign Function Interface (experimental)

Runtime permission checking:

// Check if a permission is granted
if (process.permission.has("fs.read", "/etc/passwd")) {
  console.log("Can read /etc/passwd");
} else {
  console.log("No read access to /etc/passwd");
}

Caution

The permission model is experimental. While it provides useful sandboxing, do not rely on it as your sole security boundary. Combine it with OS-level sandboxing (containers, seccomp, AppArmor) for defense in depth. Native addons can bypass the permission model entirely unless --allow-addons restrictions are used.


5. Common Tasks

5.1 Create an HTTP REST API Server

import http from "node:http";

const todos = [];
let nextId = 1;

function parseBody(req) {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      try {
        resolve(body ? JSON.parse(body) : {});
      } catch (e) {
        reject(new Error("Invalid JSON"));
      }
    });
    req.on("error", reject);
  });
}

function sendJson(res, statusCode, data) {
  res.writeHead(statusCode, {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
  });
  res.end(JSON.stringify(data));
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const path = url.pathname;
  const method = req.method;

  try {
    // GET /api/todos
    if (method === "GET" && path === "/api/todos") {
      return sendJson(res, 200, todos);
    }

    // GET /api/todos/:id
    const idMatch = path.match(/^\/api\/todos\/(\d+)$/);
    if (method === "GET" && idMatch) {
      const todo = todos.find((t) => t.id === parseInt(idMatch[1]));
      if (!todo) return sendJson(res, 404, { error: "Not found" });
      return sendJson(res, 200, todo);
    }

    // POST /api/todos
    if (method === "POST" && path === "/api/todos") {
      const body = await parseBody(req);
      const todo = { id: nextId++, title: body.title, done: false };
      todos.push(todo);
      return sendJson(res, 201, todo);
    }

    // PATCH /api/todos/:id
    if (method === "PATCH" && idMatch) {
      const todo = todos.find((t) => t.id === parseInt(idMatch[1]));
      if (!todo) return sendJson(res, 404, { error: "Not found" });
      const body = await parseBody(req);
      Object.assign(todo, body);
      return sendJson(res, 200, todo);
    }

    // DELETE /api/todos/:id
    if (method === "DELETE" && idMatch) {
      const index = todos.findIndex((t) => t.id === parseInt(idMatch[1]));
      if (index === -1) return sendJson(res, 404, { error: "Not found" });
      todos.splice(index, 1);
      return sendJson(res, 204, null);
    }

    sendJson(res, 404, { error: "Not found" });
  } catch (err) {
    sendJson(res, 400, { error: err.message });
  }
});

server.listen(3000, () => console.log("API on http://localhost:3000"));

5.2 Read and Write JSON Files

import { readFile, writeFile } from "node:fs/promises";

// Read JSON
async function loadConfig(path) {
  const raw = await readFile(path, "utf-8");
  return JSON.parse(raw);
}

// Write JSON (pretty-printed)
async function saveConfig(path, data) {
  await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
}

// Atomic write (write to temp, then rename)
import { rename } from "node:fs/promises";
import { randomBytes } from "node:crypto";

async function atomicWriteJson(filePath, data) {
  const tmp = `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
  await writeFile(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
  await rename(tmp, filePath);
}

// Usage
const config = await loadConfig("./config.json");
config.updatedAt = new Date().toISOString();
await atomicWriteJson("./config.json", config);

5.3 Parse Command-Line Arguments with util.parseArgs

import { parseArgs } from "node:util";

const { values, positionals } = parseArgs({
  options: {
    name: { type: "string", short: "n" },
    verbose: { type: "boolean", short: "v", default: false },
    port: { type: "string", short: "p", default: "3000" },
    help: { type: "boolean", short: "h", default: false },
  },
  allowPositionals: true,
  strict: true,
});

if (values.help) {
  console.log(`
Usage: node cli.js [options] <files...>

Options:
  -n, --name <name>    Your name
  -v, --verbose        Enable verbose output
  -p, --port <port>    Port number (default: 3000)
  -h, --help           Show help
  `);
  process.exit(0);
}

console.log("Name:", values.name);
console.log("Verbose:", values.verbose);
console.log("Port:", values.port);
console.log("Files:", positionals);
node cli.js --name "Node" -v -p 8080 file1.txt file2.txt
Name: Node
Verbose: true
Port: 8080
Files: [ 'file1.txt', 'file2.txt' ]

5.4 Make HTTP Requests with fetch

// GET request
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const post = await response.json();
console.log(post.title);

// POST with JSON body
const newPost = await fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    title: "New Post",
    body: "Content here",
    userId: 1,
  }),
});
console.log("Created:", newPost.status); // 201

// With timeout using AbortController
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch("https://api.example.com/data", {
    signal: controller.signal,
  });
  const data = await res.json();
  console.log(data);
} catch (err) {
  if (err.name === "AbortError") {
    console.error("Request timed out");
  } else {
    throw err;
  }
} finally {
  clearTimeout(timeout);
}

// Stream a large response
import { createWriteStream } from "node:fs";

const res = await fetch("https://example.com/large-file");
const fileStream = createWriteStream("./downloaded.bin");
for await (const chunk of res.body) {
  fileStream.write(chunk);
}
fileStream.end();

5.5 Work with Environment Variables

// Read environment variables
const port = process.env.PORT || 3000;
const nodeEnv = process.env.NODE_ENV || "development";
const dbUrl = process.env.DATABASE_URL;

if (!dbUrl) {
  console.error("DATABASE_URL is required");
  process.exit(1);
}

// Check environment
const isDev = nodeEnv === "development";
const isProd = nodeEnv === "production";

// Set at runtime (affects current process only)
process.env.MY_VAR = "value";

// Use with --env-file
// node --env-file=.env app.js

// Pattern: configuration object
const config = {
  port: parseInt(process.env.PORT || "3000", 10),
  host: process.env.HOST || "0.0.0.0",
  db: {
    url: process.env.DATABASE_URL,
    pool: parseInt(process.env.DB_POOL_SIZE || "10", 10),
  },
  logLevel: process.env.LOG_LEVEL || "info",
};

5.6 Use async/await with fs.promises

import { readFile, writeFile, readdir, stat, mkdir } from "node:fs/promises";
import path from "node:path";

// Process all JSON files in a directory
async function processJsonFiles(dir) {
  const entries = await readdir(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);

    if (entry.isDirectory()) {
      await processJsonFiles(fullPath); // Recurse
      continue;
    }

    if (path.extname(entry.name) !== ".json") continue;

    const content = JSON.parse(await readFile(fullPath, "utf-8"));
    console.log(`${entry.name}: ${Object.keys(content).length} keys`);
  }
}

// Parallel file operations
const files = ["a.txt", "b.txt", "c.txt"];
const contents = await Promise.all(
  files.map((f) => readFile(f, "utf-8"))
);

// Copy directory recursively
async function copyDir(src, dest) {
  await mkdir(dest, { recursive: true });
  const entries = await readdir(src, { withFileTypes: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    if (entry.isDirectory()) {
      await copyDir(srcPath, destPath);
    } else {
      await writeFile(destPath, await readFile(srcPath));
    }
  }
}

5.7 Stream Large Files

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";

// Copy a large file efficiently
await pipeline(
  createReadStream("input.dat"),
  createWriteStream("output.dat")
);

// Process a large CSV line by line
import { createInterface } from "node:readline";

const rl = createInterface({
  input: createReadStream("data.csv"),
  crlfDelay: Infinity,
});

let lineNum = 0;
for await (const line of rl) {
  lineNum++;
  if (lineNum === 1) continue; // Skip header
  const [name, email, age] = line.split(",");
  // Process each row...
}
console.log(`Processed ${lineNum - 1} records`);

// Transform stream: convert CSV to JSON lines
const csvToJson = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    const line = chunk.toString().trim();
    const [name, email, age] = line.split(",");
    callback(null, JSON.stringify({ name, email, age: parseInt(age) }) + "\n");
  },
});

await pipeline(
  createReadStream("data.csv"),
  csvToJson,
  createWriteStream("data.jsonl")
);

5.8 Create a WebSocket Client

Node.js includes a built-in WebSocket client (global, no import needed) since v21, following the browser WebSocket API:

const ws = new WebSocket("ws://localhost:8080");

ws.addEventListener("open", () => {
  console.log("Connected");
  ws.send("Hello, server!");
});

ws.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});

ws.addEventListener("close", () => {
  console.log("Disconnected");
});

ws.addEventListener("error", (err) => {
  console.error("WebSocket error:", err);
});

Note

Node.js includes a built-in WebSocket client since v21. For a WebSocket server, you need a third-party library such as ws. The built-in client follows the browser WebSocket API standard.

5.9 Run Tests with the Built-in Test Runner

// test/api.test.js
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import http from "node:http";

describe("API Integration Tests", () => {
  let server;
  let baseUrl;

  before((_, done) => {
    server = http.createServer((req, res) => {
      if (req.url === "/health") {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ status: "ok" }));
      } else {
        res.writeHead(404);
        res.end();
      }
    });
    server.listen(0, () => {
      baseUrl = `http://localhost:${server.address().port}`;
      done();
    });
  });

  after((_, done) => {
    server.close(done);
  });

  it("GET /health returns 200", async () => {
    const res = await fetch(`${baseUrl}/health`);
    assert.strictEqual(res.status, 200);

    const body = await res.json();
    assert.strictEqual(body.status, "ok");
  });

  it("GET /unknown returns 404", async () => {
    const res = await fetch(`${baseUrl}/unknown`);
    assert.strictEqual(res.status, 404);
  });
});

Run:

node --test test/api.test.js

5.10 Use Worker Threads for CPU-Intensive Tasks

// hash-worker.js
import { parentPort } from "node:worker_threads";
import { createHash } from "node:crypto";

parentPort.on("message", ({ algorithm, data }) => {
  const hash = createHash(algorithm).update(data).digest("hex");
  parentPort.postMessage(hash);
});
// main.js
import { Worker } from "node:worker_threads";

function hashInWorker(algorithm, data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./hash-worker.js");
    worker.postMessage({ algorithm, data });
    worker.on("message", resolve);
    worker.on("error", reject);
  });
}

// Hash large data without blocking the event loop
const hash = await hashInWorker("sha256", "a".repeat(10_000_000));
console.log("Hash:", hash);

5.11 Create a CLI Tool

#!/usr/bin/env node
// bin/mytool.js

import { parseArgs } from "node:util";
import { readFile, writeFile } from "node:fs/promises";

const { values, positionals } = parseArgs({
  options: {
    output: { type: "string", short: "o" },
    format: { type: "string", short: "f", default: "json" },
    pretty: { type: "boolean", default: false },
    help: { type: "boolean", short: "h", default: false },
    version: { type: "boolean", short: "V", default: false },
  },
  allowPositionals: true,
});

if (values.version) {
  const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf-8"));
  console.log(pkg.version);
  process.exit(0);
}

if (values.help || positionals.length === 0) {
  console.log(`
Usage: mytool [options] <input-files...>

Options:
  -o, --output <file>     Output file (default: stdout)
  -f, --format <format>   Output format: json, csv (default: json)
  --pretty                Pretty-print output
  -h, --help              Show this help
  -V, --version           Show version
  `);
  process.exit(0);
}

// Process input files
const results = [];
for (const inputFile of positionals) {
  const content = await readFile(inputFile, "utf-8");
  results.push({ file: inputFile, lines: content.split("\n").length });
}

// Format output
let output;
if (values.format === "csv") {
  output = "file,lines\n" + results.map((r) => `${r.file},${r.lines}`).join("\n");
} else {
  output = values.pretty
    ? JSON.stringify(results, null, 2)
    : JSON.stringify(results);
}

// Write output
if (values.output) {
  await writeFile(values.output, output + "\n");
  console.error(`Written to ${values.output}`);
} else {
  console.log(output);
}

Add to package.json:

{
  "bin": {
    "mytool": "./bin/mytool.js"
  }
}

Make executable and link:

chmod +x bin/mytool.js
npm link
mytool --help

5.12 Handle Signals and Graceful Shutdown

import http from "node:http";

const server = http.createServer((req, res) => {
  res.end("OK");
});

const connections = new Set();

server.on("connection", (conn) => {
  connections.add(conn);
  conn.on("close", () => connections.delete(conn));
});

function shutdown(signal) {
  console.log(`\n${signal} received. Shutting down gracefully...`);

  // Stop accepting new connections
  server.close(() => {
    console.log("Server closed. Exiting.");
    process.exit(0);
  });

  // Close existing connections after a grace period
  setTimeout(() => {
    console.warn("Forcing shutdown after timeout");
    for (const conn of connections) {
      conn.destroy();
    }
    process.exit(1);
  }, 10000);
}

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

// Handle uncaught exceptions
process.on("uncaughtException", (err) => {
  console.error("Uncaught exception:", err);
  shutdown("uncaughtException");
});

// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled rejection at:", promise, "reason:", reason);
  shutdown("unhandledRejection");
});

server.listen(3000, () => {
  console.log("Server listening on port 3000");
});

5.13 Use diagnostics_channel for Observability

import diagnostics_channel from "node:diagnostics_channel";

// Create a channel
const httpChannel = diagnostics_channel.channel("app:http");
const dbChannel = diagnostics_channel.channel("app:db");

// Publisher (in your HTTP handler)
function handleRequest(req, res) {
  const start = performance.now();

  res.on("finish", () => {
    httpChannel.publish({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: performance.now() - start,
    });
  });

  res.end("OK");
}

// Subscriber (in your monitoring setup)
diagnostics_channel.subscribe("app:http", (message) => {
  console.log(`[HTTP] ${message.method} ${message.url} -> ${message.status} (${message.duration.toFixed(1)}ms)`);
});

// Subscribe to built-in Node.js channels
diagnostics_channel.subscribe("http.server.request.start", (message) => {
  console.log("Incoming request:", message.request.url);
});

diagnostics_channel.subscribe("http.client.request.start", (message) => {
  console.log("Outgoing request:", message.request.path);
});

// Tracing integration pattern
class Tracer {
  constructor() {
    diagnostics_channel.tracingChannel("http.server.request").subscribe({
      start(message) {
        message.span = { id: crypto.randomUUID(), startTime: Date.now() };
      },
      end(message) {
        const duration = Date.now() - message.span.startTime;
        console.log(`Request ${message.span.id} completed in ${duration}ms`);
      },
      error(message) {
        console.error(`Request ${message.span.id} failed:`, message.error);
      },
    });
  }
}

5.14 Profile with --prof and --cpu-prof

V8 CPU Profile (Chrome DevTools Format)

# Generate a CPU profile
node --cpu-prof --cpu-prof-interval=100 app.js
# Press Ctrl+C to stop; creates CPU.*.cpuprofile

# Specify output directory
node --cpu-prof --cpu-prof-dir=./profiles app.js

# Open in Chrome DevTools:
# 1. Open chrome://inspect
# 2. Click "Open dedicated DevTools for Node"
# 3. Go to "Performance" tab -> Load profile

V8 Tick Profiler

# Generate V8 log
node --prof app.js
# Creates isolate-0x*.log

# Process the log
node --prof-process isolate-0x*.log > profile.txt

The output shows statistical profiling data:

 [Summary]:
   ticks  total  nonlib   name
    354   35.4%   49.3%  JavaScript
    298   29.8%   41.5%  C++
     66    6.6%    9.2%  GC
    247   24.7%          Shared libraries
    101   10.1%          Unaccounted

 [JavaScript]:
   ticks  total  nonlib   name
    127   12.7%   17.7%  LazyCompile: *processData app.js:15:21
     85    8.5%   11.8%  LazyCompile: *transform app.js:42:18

Heap Snapshot

# Generate heap snapshot on signal
node --heapsnapshot-signal=SIGUSR2 app.js

# In another terminal:
kill -USR2 <pid>
# Creates Heap.*.heapsnapshot - open in Chrome DevTools Memory tab

5.15 Use SQLite (node:sqlite)

Node.js includes a built-in SQLite module:

import { DatabaseSync } from "node:sqlite";

// Open a database (creates file if it doesn't exist)
const db = new DatabaseSync("myapp.db");

// Create tables
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// Insert with prepared statement
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "[email removed]");
insert.run("Bob", "[email removed]");

// Query
const select = db.prepare("SELECT * FROM users WHERE name = ?");
const user = select.get("Alice");
console.log(user);
// { id: 1, name: 'Alice', email: '[email removed]', created_at: '2026-05-13 ...' }

// Query all
const all = db.prepare("SELECT * FROM users");
const users = all.all();
console.log(users.length); // 2

// Close when done
db.close();

Note

The node:sqlite module provides synchronous APIs via DatabaseSync. It is suitable for embedded databases, CLI tools, and local data storage. For high-concurrency server applications, consider enabling WAL mode: db.exec("PRAGMA journal_mode=WAL").

5.16 Watch Mode for Development

# Watch and restart on changes to imported files
node --watch server.js

# Watch specific directories
node --watch-path=./src --watch-path=./config server.js

# Combine with other flags
node --watch --env-file=.env server.js

# Watch mode with test runner
node --test --watch

Watch mode output:

Server running at http://localhost:3000
[watch] restarting due to changes in ./src/routes.js...
Server running at http://localhost:3000

5.17 Load .env Files

# Load a single .env file
node --env-file=.env app.js

# Load multiple (later overrides earlier)
node --env-file=.env --env-file=.env.local app.js

# Combine with other flags
node --env-file=.env --watch server.js

Create .env:

PORT=3000
DATABASE_URL=postgres://localhost:5432/myapp
SECRET_KEY=dev-secret
NODE_ENV=development

Access in code:

console.log(process.env.PORT);         // "3000"
console.log(process.env.DATABASE_URL); // "postgres://localhost:5432/myapp"

5.18 Build Single Executable Applications

Package a Node.js app into a single binary that includes the Node.js runtime:

Step 1: Create a configuration file sea-config.json:

{
  "main": "app.js",
  "output": "sea-prep.blob",
  "disableExperimentalSEAWarning": true
}

Step 2: Generate the preparation blob:

node --experimental-sea-config sea-config.json

Step 3: Copy the Node.js binary:

# Linux/macOS
cp $(which node) myapp

# Windows
copy node.exe myapp.exe

Step 4: Inject the blob into the binary:

# Linux
npx postject myapp NODE_SEA_BLOB sea-prep.blob \
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

# macOS
npx postject myapp NODE_SEA_BLOB sea-prep.blob \
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
  --macho-segment-name NODE_SEA

# Windows
npx postject myapp.exe NODE_SEA_BLOB sea-prep.blob ^
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

Step 5: Run the executable:

./myapp

The resulting binary is self-contained — no Node.js installation needed on the target machine.

5.19 Cluster for Multi-Core Scaling

import cluster from "node:cluster";
import http from "node:http";
import { availableParallelism } from "node:os";

const numCPUs = availableParallelism();

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} starting ${numCPUs} workers`);

  // Fork workers
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // Restart workers that crash
  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
    cluster.fork();
  });

  // Graceful shutdown
  process.on("SIGTERM", () => {
    console.log("SIGTERM received, shutting down workers...");
    for (const id in cluster.workers) {
      cluster.workers[id].process.kill("SIGTERM");
    }
  });
} else {
  // Workers share the TCP connection
  const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Worker ${process.pid} handled this request\n`);
  });

  server.listen(3000, () => {
    console.log(`Worker ${process.pid} started`);
  });
}
Primary 12345 starting 8 workers
Worker 12346 started
Worker 12347 started
Worker 12348 started
...

5.20 Use the REPL Effectively

# Start with custom prompt
NODE_REPL_HISTORY=~/.node_history node

# Preload modules
node -e "globalThis.fs = require('node:fs'); globalThis.path = require('node:path')" -i

# REPL from within your app
import repl from "node:repl";

const server = repl.start({
  prompt: "myapp> ",
  useColors: true,
});

// Add custom commands
server.defineCommand("status", {
  help: "Show application status",
  action() {
    console.log("Memory:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
    console.log("Uptime:", process.uptime(), "seconds");
    this.displayPrompt();
  },
});

REPL tips:

  • _ holds the result of the last expression
  • _error holds the last error
  • Tab completion works for all properties
  • .editor for multi-line input
  • .load and .save for session management

5.21 TLS/SSL Server Setup

import tls from "node:tls";
import { readFileSync } from "node:fs";

const options = {
  key: readFileSync("server-key.pem"),
  cert: readFileSync("server-cert.pem"),
  ca: [readFileSync("ca-cert.pem")], // CA certificates for client verification
  requestCert: false,                 // Set true for mutual TLS
  minVersion: "TLSv1.2",
};

const server = tls.createServer(options, (socket) => {
  console.log("Client connected:", socket.authorized ? "authorized" : "unauthorized");
  socket.write("Welcome!\n");
  socket.pipe(socket); // Echo server
});

server.listen(8443, () => {
  console.log("TLS server listening on port 8443");
});

// Generate self-signed cert for testing:
// openssl req -x509 -newkey rsa:4096 -keyout server-key.pem -out server-cert.pem -days 365 -nodes

5.22 UDP / Datagram Sockets

import dgram from "node:dgram";

// UDP Server
const server = dgram.createSocket("udp4");

server.on("message", (msg, rinfo) => {
  console.log(`Received: ${msg} from ${rinfo.address}:${rinfo.port}`);
  server.send(`Echo: ${msg}`, rinfo.port, rinfo.address);
});

server.on("listening", () => {
  const addr = server.address();
  console.log(`UDP server listening on ${addr.address}:${addr.port}`);
});

server.bind(41234);

// UDP Client
const client = dgram.createSocket("udp4");
const message = Buffer.from("Hello UDP");
client.send(message, 41234, "localhost", (err) => {
  if (err) console.error(err);
});

client.on("message", (msg) => {
  console.log(`Response: ${msg}`);
  client.close();
});

5.23 Compress and Decompress with zlib

import { createGzip, createGunzip, gzip, gunzip } from "node:zlib";
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { promisify } from "node:util";

// Stream-based compression (for large files)
await pipeline(
  createReadStream("input.log"),
  createGzip(),
  createWriteStream("input.log.gz")
);

// Stream-based decompression
await pipeline(
  createReadStream("input.log.gz"),
  createGunzip(),
  createWriteStream("input.log.restored")
);

// Buffer-based (for small data)
const gzipAsync = promisify(gzip);
const gunzipAsync = promisify(gunzip);

const compressed = await gzipAsync(Buffer.from("Hello, World!"));
console.log("Compressed size:", compressed.length);

const decompressed = await gunzipAsync(compressed);
console.log("Decompressed:", decompressed.toString()); // "Hello, World!"

// HTTP response compression
import http from "node:http";

http.createServer((req, res) => {
  const acceptEncoding = req.headers["accept-encoding"] || "";

  const body = "Response data here...".repeat(100);

  if (acceptEncoding.includes("gzip")) {
    res.writeHead(200, {
      "Content-Type": "text/plain",
      "Content-Encoding": "gzip",
    });
    const gzStream = createGzip();
    gzStream.pipe(res);
    gzStream.end(body);
  } else {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end(body);
  }
}).listen(3000);

5.24 Source Map Support

# Enable source maps for better stack traces with transpiled code
node --enable-source-maps app.js

When using TypeScript or a bundler, source maps let you see original file names and line numbers in stack traces:

// Without --enable-source-maps:
// Error at dist/app.js:42:15

// With --enable-source-maps:
// Error at src/app.ts:28:10

Set via environment variable:

export NODE_OPTIONS="--enable-source-maps"

5.25 Use the Permission Model for Sandboxing

# Run an untrusted script with minimal permissions
node --experimental-permission \
  --allow-fs-read=./data \
  untrusted-script.js

# A data processing tool that only reads input/ and writes to output/
node --experimental-permission \
  --allow-fs-read=./input \
  --allow-fs-write=./output \
  process-data.js

# A web server that can read its own directory but cannot spawn processes
node --experimental-permission \
  --allow-fs-read=/app \
  --allow-fs-write=/app/logs \
  server.js

Check permissions programmatically:

// Detect if permission model is active
if (process.permission) {
  console.log("Permission model is active");

  // Check specific permissions
  console.log("Can read /etc:", process.permission.has("fs.read", "/etc"));
  console.log("Can write /tmp:", process.permission.has("fs.write", "/tmp"));
  console.log("Can spawn:", process.permission.has("child"));
  console.log("Can use workers:", process.permission.has("worker"));
}

5.26 Process and OS Information

import os from "node:os";

// System info
console.log("Hostname:", os.hostname());
console.log("Platform:", os.platform());     // 'linux', 'darwin', 'win32'
console.log("Architecture:", os.arch());      // 'x64', 'arm64'
console.log("CPUs:", os.availableParallelism());
console.log("Total memory:", (os.totalmem() / 1024 ** 3).toFixed(1), "GB");
console.log("Free memory:", (os.freemem() / 1024 ** 3).toFixed(1), "GB");
console.log("Uptime:", (os.uptime() / 3600).toFixed(1), "hours");
console.log("Home dir:", os.homedir());
console.log("Temp dir:", os.tmpdir());

// CPU details
for (const cpu of os.cpus()) {
  console.log(`  ${cpu.model} @ ${cpu.speed} MHz`);
}

// Network interfaces
const nets = os.networkInterfaces();
for (const [name, addrs] of Object.entries(nets)) {
  for (const addr of addrs) {
    if (addr.family === "IPv4" && !addr.internal) {
      console.log(`${name}: ${addr.address}`);
    }
  }
}

// Process info
console.log("PID:", process.pid);
console.log("Node version:", process.version);
console.log("V8 version:", process.versions.v8);
console.log("Uptime:", process.uptime(), "seconds");
console.log("Memory:", process.memoryUsage());
// { rss: 30M, heapTotal: 6M, heapUsed: 4M, external: 1M, arrayBuffers: 0 }

5.27 Readline for Interactive Input

import { stdin, stdout } from "node:process";

// Promise-based (Node.js 17+)
import { createInterface } from "node:readline/promises";

const rl = createInterface({ input: stdin, output: stdout });
const name = await rl.question("What is your name? ");
console.log(`Hello, ${name}!`);
rl.close();

// Interactive menu
async function menu() {
  const rl = createInterface({ input: stdin, output: stdout });

  while (true) {
    console.log("\n1. List files");
    console.log("2. Show date");
    console.log("3. Exit");
    const choice = await rl.question("\nChoice: ");

    switch (choice) {
      case "1":
        const { readdir } = await import("node:fs/promises");
        console.log((await readdir(".")).join(", "));
        break;
      case "2":
        console.log(new Date().toISOString());
        break;
      case "3":
        rl.close();
        return;
      default:
        console.log("Invalid choice");
    }
  }
}

5.28 Assert for Validation

import assert from "node:assert/strict";

// Strict equality
assert.strictEqual(1 + 1, 2);
assert.strictEqual("hello", "hello");

// Deep equality (objects and arrays)
assert.deepStrictEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] });

// Truthy/falsy
assert.ok(true);
assert.ok(1);

// Throws
assert.throws(() => {
  throw new Error("expected");
}, { message: "expected" });

// Rejects (async)
await assert.rejects(
  async () => { throw new Error("async error"); },
  { message: "async error" }
);

// Does not throw
assert.doesNotThrow(() => {
  return 42;
});

// Match (regex)
assert.match("hello world", /world/);
assert.doesNotMatch("hello world", /xyz/);

5.29 Performance Measurement

import { performance, PerformanceObserver } from "node:perf_hooks";

// Basic timing
const start = performance.now();
// ... work ...
const duration = performance.now() - start;
console.log(`Took ${duration.toFixed(2)}ms`);

// Performance marks and measures
performance.mark("start-db-query");
// ... database query ...
performance.mark("end-db-query");
performance.measure("db-query", "start-db-query", "end-db-query");

// Observe measurements
const obs = new PerformanceObserver((items) => {
  for (const entry of items.getEntries()) {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  }
});
obs.observe({ entryTypes: ["measure"] });

// Function timing wrapper
function timeFunction(fn, label) {
  return function (...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    console.log(`${label}: ${(performance.now() - start).toFixed(2)}ms`);
    return result;
  };
}

// timerify — automatic function timing
const timedFunction = performance.timerify(myExpensiveFunction);
obs.observe({ entryTypes: ["function"] });
timedFunction(); // Automatically measured

5.30 Async Hooks for Context Tracking

import { AsyncLocalStorage } from "node:async_hooks";

const asyncLocalStorage = new AsyncLocalStorage();

// Request ID tracking middleware pattern
function handleRequest(req, res) {
  const requestId = crypto.randomUUID();

  asyncLocalStorage.run({ requestId }, () => {
    // Everything in this callback and its async descendants
    // can access the request ID
    processRequest(req, res);
  });
}

function processRequest(req, res) {
  const store = asyncLocalStorage.getStore();
  log(`Processing request ${store.requestId}`);
  // Even through async boundaries, the store is available
  setTimeout(() => {
    const store = asyncLocalStorage.getStore();
    log(`Still have request ID: ${store.requestId}`);
    res.end("OK");
  }, 100);
}

function log(message) {
  const store = asyncLocalStorage.getStore();
  const prefix = store?.requestId ? `[${store.requestId}]` : "[no-context]";
  console.log(`${prefix} ${message}`);
}

6. Troubleshooting

6.1 Error: Cannot find module '...'

Cause: Node.js cannot locate the module you are trying to require() or import. The module may not be installed, the path may be wrong, or the module resolution mode (CJS vs ESM) may be mismatched.

Solution:

  1. Check that the package is installed: npm ls <package-name>
  2. Run npm install to install missing dependencies
  3. Verify the import path is correct (relative paths need ./)
  4. Check that "type": "module" in package.json matches your syntax

Prevention: Use a lockfile (package-lock.json) and run npm ci in CI environments.

6.2 Error: ERR_REQUIRE_ESM

Cause: You used require() to load an ES Module package. Many modern packages are ESM-only.

Solution:

  1. Switch to import: import pkg from 'package-name'
  2. Set "type": "module" in your package.json
  3. Or use dynamic import: const pkg = await import('package-name')

Prevention: When starting new projects, use "type": "module" from the start.

6.3 Error: ERR_MODULE_NOT_FOUND with import

Cause: ES module imports require full file extensions. Unlike require(), import does not auto-resolve .js or index.js.

Solution:

// Wrong
import { utils } from "./utils";

// Correct
import { utils } from "./utils.js";

Prevention: Always include file extensions in ES module imports.

6.4 Error: SyntaxError: Cannot use import statement outside a module

Cause: You used import syntax in a file that Node.js treats as CommonJS.

Solution:

  1. Add "type": "module" to your package.json
  2. Or rename the file to .mjs
  3. Or switch to require() syntax

6.5 Error: RangeError: Maximum call stack size exceeded

Cause: Infinite recursion or extremely deep recursion.

Solution:

  1. Check for recursive function calls without proper base cases
  2. Use iterative algorithms for deep data structures
  3. Increase stack size if legitimately needed: node --stack-size=8192 app.js

Prevention: Always validate recursion base cases. Use tail-call patterns or convert to iterative loops.

6.6 Error: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

Cause: V8 ran out of heap memory. Default is approximately 1.5 GB for 64-bit systems.

Solution:

node --max-old-space-size=4096 app.js  # 4 GB heap

Or via environment variable:

export NODE_OPTIONS="--max-old-space-size=4096"

Prevention: Profile memory usage, use streams instead of loading entire files into memory, avoid memory leaks by properly removing event listeners and clearing intervals.

6.7 Error: ECONNREFUSED or ECONNRESET

Cause: Connection to a remote service was refused or reset. The target server may be down, the port may be wrong, or a firewall is blocking the connection.

Solution:

  1. Verify the target service is running: curl http://localhost:3000
  2. Check the host and port in your connection string
  3. Check firewall rules
  4. Verify DNS resolution: node -e "require('dns').lookup('hostname', console.log)"

Prevention: Implement retry logic with exponential backoff for external service calls.

6.8 Error: EADDRINUSE: address already in use :::3000

Cause: Another process is already listening on the specified port.

Solution:

# Find what's using the port
# Linux/macOS
lsof -i :3000
# Windows
netstat -ano | findstr :3000

# Kill the process
kill <PID>

# Or use a different port
PORT=3001 node server.js

Prevention: Make the port configurable via environment variable. Handle the error gracefully:

server.on("error", (err) => {
  if (err.code === "EADDRINUSE") {
    console.error(`Port ${port} is in use. Try: PORT=${port + 1} node server.js`);
    process.exit(1);
  }
});

6.9 Error: EMFILE: too many open files

Cause: The process has exceeded the OS limit for open file descriptors.

Solution:

# Check current limit
ulimit -n

# Increase limit (Linux/macOS)
ulimit -n 65536

# Permanent change (Linux): add to /etc/security/limits.conf
# * soft nofile 65536
# * hard nofile 65536

Prevention: Use streaming APIs instead of opening many files simultaneously. Limit concurrency with a semaphore pattern.

6.10 Error: ERR_OSSL_EVP_UNSUPPORTED

Cause: OpenSSL 3.0 (bundled in Node.js 17+) removed support for some legacy cryptographic algorithms used by older packages.

Solution:

node --openssl-legacy-provider app.js

Prevention: Update packages that rely on legacy algorithms. Use npm audit to identify outdated dependencies.

6.11 Error: ENOSPC: System limit for number of file watchers reached

Cause: Linux inotify watcher limit reached. Common when using --watch mode or tools that watch many files.

Solution:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Prevention: Set the higher limit in your system configuration or Docker image.

6.12 Error: ERR_HTTP_HEADERS_SENT

Cause: Attempting to set headers or send a response after the response has already been sent.

Solution:

// Wrong — sends headers twice
function handler(req, res) {
  if (condition) {
    res.json({ ok: true });
    // Missing return! Falls through to next response
  }
  res.json({ error: "not ok" });
}

// Correct
function handler(req, res) {
  if (condition) {
    return res.json({ ok: true });
  }
  res.json({ error: "not ok" });
}

Prevention: Always return after sending a response in conditional branches.

6.13 Error: UnhandledPromiseRejection

Cause: A Promise was rejected but no .catch() handler or try/catch block caught the error.

Solution:

// Add catch handler
fetchData().catch((err) => console.error("Failed:", err));

// Or use try/catch with async/await
try {
  await fetchData();
} catch (err) {
  console.error("Failed:", err);
}

Prevention: Enable the --unhandled-rejections=throw flag (default since Node.js 15) to crash on unhandled rejections. Add a global handler for logging:

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1);
});

6.14 Error: DEPTH_ZERO_SELF_SIGNED_CERT or UNABLE_TO_VERIFY_LEAF_SIGNATURE

Cause: TLS connection to a server with a self-signed or improperly chained certificate.

Solution:

// For a specific request (preferred)
import https from "node:https";
import { readFileSync } from "node:fs";

const agent = new https.Agent({
  ca: readFileSync("./custom-ca.pem"),
});

// Or add custom CA globally
// NODE_EXTRA_CA_CERTS=./custom-ca.pem node app.js

Prevention: Use NODE_EXTRA_CA_CERTS for internal CAs. Never set NODE_TLS_REJECT_UNAUTHORIZED=0 in production.

6.15 Error: ETIMEOUT or ETIMEDOUT

Cause: A network operation timed out — DNS resolution, TCP connection, or HTTP response took too long.

Solution:

  1. Check network connectivity
  2. Increase timeout values
  3. Add retry logic
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);

try {
  const response = await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

6.16 Error: ENOMEM: not enough memory

Cause: The system ran out of physical memory or the container memory limit was hit.

Solution:

  1. Increase container memory limits
  2. Reduce --max-old-space-size to stay within limits
  3. Profile memory usage: node --inspect app.js and use Chrome DevTools Memory tab

Prevention: Set explicit memory limits and monitor with process.memoryUsage().

6.17 Warning: MaxListenersExceededWarning

Cause: More than 10 listeners added to a single event. Usually indicates a memory leak from adding listeners in a loop without removing them.

Solution:

// If intentional, increase the limit
emitter.setMaxListeners(25);

// If a leak, remove listeners when done
const handler = () => { /* ... */ };
emitter.on("event", handler);
// Later:
emitter.off("event", handler);

Prevention: Use once() for one-time listeners. Track listener additions in loops.

6.18 Error: EACCES: permission denied

Cause: The process does not have file system permissions to access the path, or on Linux, binding to ports below 1024 requires root.

Solution:

# Fix file permissions
chmod 644 config.json
chown $(whoami) data/

# For port 80/443, use a reverse proxy (nginx/caddy) instead of running as root

6.19 Error: ERR_WORKER_OUT_OF_MEMORY

Cause: A worker thread exceeded its memory limit.

Solution:

const worker = new Worker("./worker.js", {
  resourceLimits: {
    maxOldGenerationSizeMb: 512,
    maxYoungGenerationSizeMb: 64,
  },
});

6.20 Error: CERT_HAS_EXPIRED

Cause: The TLS certificate of the remote server has expired.

Solution:

  1. Contact the server administrator to renew the certificate
  2. For testing only: NODE_TLS_REJECT_UNAUTHORIZED=0 node app.js

Prevention: Monitor certificate expiration dates.

6.21 Error: dns.resolveSrv ECONNREFUSED on Windows

Cause: This is a known issue (GitHub #62326). DNS SRV resolution can fail with ECONNREFUSED on Windows systems due to how the c-ares resolver interacts with Windows DNS.

Solution:

  1. Use dns.lookup() instead if possible (uses OS resolver)
  2. Set a custom DNS server: dns.setServers(['8.8.8.8'])
  3. Retry with a fallback resolver

6.22 Stack Traces Broken with --enable-source-maps

Cause: Known issue (GitHub #55236). Source map support can produce incorrect or missing stack trace lines in certain cases.

Solution:

  1. Verify your source maps are valid (use a source map validator to check)
  2. Try without --enable-source-maps to get V8's raw traces
  3. Use --inspect and Chrome DevTools which have independent source map handling

6.23 Composed Streams Hang Since Node.js 22.5

Cause: Known regression (GitHub #60083). Composed streams using stream.compose() may hang in certain pipeline configurations.

Solution:

  1. Use pipeline() instead of stream.compose()
  2. Pin to Node.js 22.4 if you depend on composed streams
  3. Monitor the issue for a fix in a patch release

6.24 Error: ERR_INVALID_PACKAGE_CONFIG

Cause: The package.json file has invalid JSON or an invalid "exports" configuration.

Solution:

  1. Validate JSON syntax: node -e "JSON.parse(require('fs').readFileSync('package.json'))"
  2. Check "exports" map for missing entries
  3. Ensure "type" is either "module" or "commonjs"

6.25 Error: ERR_UNKNOWN_FILE_EXTENSION for .ts Files

Cause: Running a TypeScript file without the --experimental-strip-types flag.

Solution:

node --experimental-strip-types app.ts

Or register a custom loader:

node --import tsx app.ts

Prevention: Add the flag to your package.json scripts or NODE_OPTIONS.

6.26 Slow Startup Time

Cause: Loading too many modules at startup, or modules with expensive initialization.

Solution:

  1. Profile startup: node --cpu-prof --cpu-prof-interval=10 app.js (stop quickly with Ctrl+C)
  2. Lazy-load modules that are not needed immediately
  3. Use --v8-pool-size=0 to reduce V8's background thread overhead for CLI tools
  4. Consider a snapshot (V8 startup snapshot) for complex applications

6.27 Error: EPERM: operation not permitted with Permission Model

Cause: The --experimental-permission flag is active and the operation is not in the allow list.

Solution:

  1. Add the required permission flag: --allow-fs-read=/path/to/files
  2. Check which permission is needed: process.permission.has("fs.read", path)

7. FAQ

General

Q: What version of Node.js should I use?

Use the current LTS (Long-Term Support) release for production applications. LTS versions receive security updates and bug fixes for 30 months. Even-numbered major versions (18, 20, 22) become LTS. Odd-numbered versions are short-lived "Current" releases for testing new features.

Q: What is the difference between Node.js and Deno or Bun?

All three are JavaScript runtimes built on V8 (Bun uses JavaScriptCore). Node.js is the most mature with the largest ecosystem. Deno has built-in TypeScript, a permission system, and web-standard APIs by default. Bun focuses on speed and includes a bundler. Node.js has adopted many of Deno's ideas — permission model, built-in test runner, TypeScript support, fetch().

Q: Is Node.js single-threaded?

The JavaScript event loop runs on a single thread, but Node.js itself is multi-threaded. I/O operations (file system, network, DNS) are handled by a libuv thread pool (default 4 threads, configurable via UV_THREADPOOL_SIZE). You can use worker_threads for CPU-bound parallelism.

Q: How do I check which Node.js version introduced a feature?

Check the Node.js changelog or use node -p "process.features" for compile-time features. The Node.js API docs list "Added in" versions for every API.

Q: What is the difference between npm, yarn, and pnpm?

All are package managers for Node.js. npm ships with Node.js. yarn (by Facebook) introduced lockfiles and workspaces. pnpm uses a content-addressable store for disk-efficient installations. All use the same npmjs.com registry.

Modules and Dependencies

Q: Should I use CommonJS or ES Modules?

For new projects, use ES Modules ("type": "module" in package.json). ESM is the JavaScript standard, supports import/export, has better tree-shaking, and is the direction Node.js is heading. CommonJS (require) is still fully supported and necessary for some older packages.

Q: Can I use require() in ES Modules?

Not directly. Use import instead, or createRequire:

import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const pkg = require("./package.json");

Q: How do I import JSON in ES Modules?

// Option 1: Read and parse
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("./config.json", "utf-8"));

// Option 2: Import attribute (experimental)
import config from "./config.json" with { type: "json" };

Q: What does the node: prefix mean in imports?

The node: prefix explicitly imports Node.js built-in modules: import fs from "node:fs". It prevents conflicts with npm packages that have the same name. Available since Node.js 16.0.0, recommended for all new code.

Q: How do I handle circular dependencies?

CommonJS returns a partial export object when circular requires are detected. ES Modules use live bindings, which generally work but can cause undefined values if accessed before initialization. Restructure your code to avoid circular dependencies when possible — extract shared code into a separate module.

Performance

Q: How do I increase the memory limit?

node --max-old-space-size=4096 app.js  # 4 GB

The default is approximately 1.5 GB on 64-bit systems. For containers, ensure the Node.js memory limit is below the container memory limit.

Q: How many concurrent connections can Node.js handle?

Node.js can handle tens of thousands of concurrent connections on a single thread because connections are non-blocking. The practical limit depends on your OS (file descriptor limits), memory, and application complexity. Use cluster or multiple instances behind a load balancer for higher throughput.

Q: Should I use cluster or worker_threads?

Use cluster for scaling HTTP servers across CPU cores — each worker is a separate process with its own memory. Use worker_threads for CPU-intensive computation within a single application — threads share memory via SharedArrayBuffer and have lower overhead than processes.

Q: How do I detect memory leaks?

  1. Monitor process.memoryUsage().heapUsed over time
  2. Take heap snapshots: node --inspect app.js and use Chrome DevTools Memory tab
  3. Use --heap-prof for heap allocation profiles
  4. Watch for growing listener counts: emitter.listenerCount('event')

Q: What is the libuv thread pool and when should I increase it?

libuv uses a thread pool (default size: 4) for operations that cannot be done asynchronously at the OS level: DNS resolution (dns.lookup), file system operations, and some crypto operations. Increase it if your application does heavy file I/O or DNS lookups:

UV_THREADPOOL_SIZE=16 node app.js

TypeScript

Q: How do I run TypeScript without a build step?

node --experimental-strip-types app.ts

This strips type annotations at load time. It does not support enum, namespace, or features requiring code transformation.

Q: Can I use TypeScript with the built-in test runner?

Yes:

node --experimental-strip-types --test "test/**/*.test.ts"

Q: Do I still need tsc?

Yes, for type checking. --experimental-strip-types only removes types — it does not validate them. Run tsc --noEmit for type checking, and use node --experimental-strip-types for execution.

Security

Q: How do I handle secrets and API keys?

  1. Use environment variables: process.env.API_KEY
  2. Load from .env files: node --env-file=.env app.js
  3. Never commit secrets to source control
  4. In production, use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)

Q: What is the permission model and should I use it?

The permission model (--experimental-permission) restricts what your application can do: file system access, child process spawning, worker threads, and native addons. Use it when running untrusted code or as defense in depth. It is experimental — combine with OS-level sandboxing.

Q: How do I keep dependencies secure?

npm audit              # Check for known vulnerabilities
npm audit fix          # Auto-fix where possible
npm outdated           # Check for outdated packages
npm update             # Update within semver ranges

Run npm audit in CI and block deployments on critical vulnerabilities.

Q: Is eval() safe to use?

No. eval() executes arbitrary code and is a severe security risk if any part of the input comes from users. Use JSON.parse() for data, vm.runInNewContext() for sandboxed execution (with caveats), or the permission model for defense in depth.

Debugging and Diagnostics

Q: How do I debug a Node.js application?

node --inspect app.js       # Attach Chrome DevTools
node --inspect-brk app.js   # Break on first line
node inspect app.js         # CLI debugger

Open chrome://inspect in Chrome to connect. VS Code also has built-in Node.js debugging support.

Q: How do I profile CPU usage?

node --cpu-prof app.js      # Chrome DevTools .cpuprofile format
node --prof app.js          # V8 tick profile (process with --prof-process)

Q: How do I get a heap dump?

node --heapsnapshot-signal=SIGUSR2 app.js
# Then: kill -USR2 <pid>

Or programmatically:

import v8 from "node:v8";
v8.writeHeapSnapshot();

Q: What does NODE_DEBUG do?

It enables debug logging for Node.js core modules. Set it to a comma-separated list of module names:

NODE_DEBUG=http,net,tls node app.js

This prints internal debug information to stderr — useful for understanding what Node.js is doing under the hood.

Development Workflow

Q: How do I auto-restart my server during development?

node --watch server.js

Built-in since Node.js 18.11. No external tools needed.

Q: How do I use .env files?

node --env-file=.env app.js

Built-in since Node.js 20.6.

Q: How do I run a quick script without creating a file?

node -e "console.log(process.versions)"
node -p "require('os').cpus().length"

Q: Can Node.js execute shell commands?

Yes, via child_process:

import { execSync } from "node:child_process";
const result = execSync("ls -la", { encoding: "utf-8" });

Q: How do I handle graceful shutdown?

Listen for SIGTERM and SIGINT, stop accepting new connections, drain existing connections, then exit. See the Graceful Shutdown recipe in Section 5.12.

Q: How do I test my code?

Use the built-in test runner:

node --test

Or with watch mode:

node --test --watch

No external testing framework is required for most use cases.

Q: What is the node: import prefix and should I use it?

Yes. import fs from "node:fs" explicitly imports the Node.js built-in module, preventing accidental shadowing by npm packages. Always use it for built-in modules.

Deployment

Q: How should I set NODE_ENV in production?

NODE_ENV=production node server.js

Many frameworks and libraries optimize behavior when NODE_ENV=production: disabling debug output, enabling caching, and reducing logging verbosity. Always set it in production.

Q: What Docker base image should I use?

Image Size Use Case
node:22-alpine ~50 MB Production (smallest, musl libc)
node:22-slim ~80 MB Production (glibc, more compatible)
node:22 ~350 MB Development (includes build tools)

Use multi-stage builds: build in node:22, run in node:22-alpine.

Q: How do I run Node.js as a system service?

Use systemd on Linux:

[Unit]
Description=My Node.js App
After=network.target

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Or use a process manager like PM2, or run in a container orchestrator (Docker Compose, Kubernetes).