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:testmodule 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
nvmorfnmfirst — 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.jsto automatically restart the server when you edit the file. No external tools likenodemonrequired.
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, ornvmto 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-toolsor 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:
- If installed via nvm/fnm: run
nvm use defaultorfnm use default - If installed from package: check
/usr/local/bin/nodeexists - 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:
- Use a version manager (nvm/fnm) — this avoids the problem entirely
- Or configure npm to use your home directory:
npm config set prefix '~/.npm-global'and add~/.npm-global/binto 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=0disables TLS certificate verification globally. Never do this in production — it makes your application vulnerable to man-in-the-middle attacks. UseNODE_EXTRA_CA_CERTSto 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-fileflag loads variables intoprocess.envbefore 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-typesflag only removes type annotations — it does not support TypeScript features that require code transformation, such asenum,namespace, or legacy decorators. Usetscor 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-pathto 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 synchronousmodule.exportsdefines 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, andexportsare NOT available in ES Modules. Useimport.meta.urlandimport.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}`);
});
Pipeline (Recommended for Connecting Streams)
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. Usestream.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()orpath.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/hostsand 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
astype assertions- Optional chaining and nullish coalescing (these are JavaScript too)
What does NOT work (requires a build step):
enumdeclarationsnamespacedeclarations- Legacy decorators (Stage 2 decorators work)
const enum- Implicit file extensions (must use
.ts) import typetransformation
Important
Type stripping does not perform type checking. Run
tsc --noEmitseparately for type validation. The--experimental-strip-typesflag 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-addonsrestrictions 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
WebSocketclient since v21. For a WebSocket server, you need a third-party library such asws. 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:sqlitemodule provides synchronous APIs viaDatabaseSync. 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_errorholds the last error- Tab completion works for all properties
.editorfor multi-line input.loadand.savefor 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:
- Check that the package is installed:
npm ls <package-name> - Run
npm installto install missing dependencies - Verify the import path is correct (relative paths need
./) - Check that
"type": "module"inpackage.jsonmatches 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:
- Switch to
import:import pkg from 'package-name' - Set
"type": "module"in yourpackage.json - 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:
- Add
"type": "module"to yourpackage.json - Or rename the file to
.mjs - Or switch to
require()syntax
6.5 Error: RangeError: Maximum call stack size exceeded
Cause: Infinite recursion or extremely deep recursion.
Solution:
- Check for recursive function calls without proper base cases
- Use iterative algorithms for deep data structures
- 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:
- Verify the target service is running:
curl http://localhost:3000 - Check the host and port in your connection string
- Check firewall rules
- 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:
- Check network connectivity
- Increase timeout values
- 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:
- Increase container memory limits
- Reduce
--max-old-space-sizeto stay within limits - Profile memory usage:
node --inspect app.jsand 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:
- Contact the server administrator to renew the certificate
- 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:
- Use
dns.lookup()instead if possible (uses OS resolver) - Set a custom DNS server:
dns.setServers(['8.8.8.8']) - 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:
- Verify your source maps are valid (use a source map validator to check)
- Try without
--enable-source-mapsto get V8's raw traces - Use
--inspectand 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:
- Use
pipeline()instead ofstream.compose() - Pin to Node.js 22.4 if you depend on composed streams
- 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:
- Validate JSON syntax:
node -e "JSON.parse(require('fs').readFileSync('package.json'))" - Check
"exports"map for missing entries - 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:
- Profile startup:
node --cpu-prof --cpu-prof-interval=10 app.js(stop quickly with Ctrl+C) - Lazy-load modules that are not needed immediately
- Use
--v8-pool-size=0to reduce V8's background thread overhead for CLI tools - 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:
- Add the required permission flag:
--allow-fs-read=/path/to/files - 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?
- Monitor
process.memoryUsage().heapUsedover time - Take heap snapshots:
node --inspect app.jsand use Chrome DevTools Memory tab - Use
--heap-proffor heap allocation profiles - 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?
- Use environment variables:
process.env.API_KEY - Load from
.envfiles:node --env-file=.env app.js - Never commit secrets to source control
- 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).