Node.js Modern Batteries-Included Built-ins

Node.js Modern Batteries-Included Built-ins

Overview

Between Node.js v20 and v26 (2024-2026) the runtime absorbed a wave of capabilities that historically required an npm dependency. A SQLite driver, a WebSocket client, .env parsing, a task runner, file watching, glob matching, terminal colors, deep cloning, and a V8 startup cache now all ship in core. The practical upshot: many small projects can drop better-sqlite3, ws, dotenv, nodemon, chalk, glob, and lodash.cloneDeep entirely.

The catch — and the reason this reference exists — is stability is per-feature and recent. Some of these are fully Stable (2), several are Release Candidate (1.2), and a few are still Experimental (1) or “Active development” (1.1). Shipping an Experimental API to production without pinning the Node version is the cardinal sin here. Every concept below states its added-in version and current stability index explicitly; treat those as the load-bearing facts, because an API that is RC today may change a method signature in the next minor.

Scope boundaries (owned by sibling references in this family): Single-Executable Applications, the --permission model, and native TypeScript stripping live in nodejs-typescript-and-runtime-features; the deep node:test runner (mocking, coverage, reporters, snapshots) lives in nodejs-test-runnernode:test is mentioned here only as “it exists, it replaces Jest/Mocha for many projects, see the sibling”; Bun/Deno/edge equivalents live in javascript-runtimes-deno-bun-edge.

A note on reading stability: index 2 = Stable; 1.2 = Release Candidate (API frozen, shipping unflagged, final polish); 1.1 = Active development (unflagged but may change); 1 = Experimental; 1.0 = Early development. Anything below 2 deserves a pinned engines.node and a changelog read on upgrade.

Core concepts

1. node:sqlite — a built-in synchronous SQLite driver

Added in v22.5.0, unflagged since v23.4.0 / v22.13.0 (was behind --experimental-sqlite), currently Stability 1.2 - Release Candidate. Available only under the node: scheme. It is the in-core analogue of better-sqlite3: synchronous, prepared-statement-centric, fast.

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');               // or a path, or a Buffer/URL
db.exec('CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT) STRICT');

const insert = db.prepare('INSERT INTO users (id, name) VALUES (?, ?)');
insert.run(1, 'Ada');                                  // { changes: 1, lastInsertRowid: 1 }

const byId = db.prepare('SELECT * FROM users WHERE id = ?');
byId.get(1);                                           // { id: 1, name: 'Ada' } | undefined
db.prepare('SELECT * FROM users').all();               // [{ id, name }, ...]
for (const row of byId.iterate(1)) { /* streaming */ } // iterate added v23.4.0/v22.13.0

2. The global WebSocket client (undici-backed)

A spec-compliant, browser-compatible WebSocket is exposed on the global scope, backed by undici. Timeline: experimental behind --experimental-websocket in v21, on by default in v22.0.0 (disable with --no-experimental-websocket), and no longer experimental as of v22.4.0. No import needed.

const ws = new WebSocket('wss://example.com/feed');
ws.addEventListener('open',   () => ws.send('hello'));
ws.addEventListener('message', (e) => console.log(e.data));
ws.addEventListener('error',  (e) => console.error(e));
ws.addEventListener('close',  () => {});

It replaces the ws package for client use only. Critically, there is no built-in WebSocket server — to accept connections you still need ws (or another library). The API is the WHATWG/browser WebSocket, not the ws EventEmitter API, so it is portable to browsers but is not a drop-in for ws’s .on('message') server-side idioms.

3. Environment files — --env-file, loadEnvFile(), util.parseEnv()

The in-core replacement for dotenv. Three surfaces:

.env parsing rules: KEY=value per line; text after # is a comment; values may be quoted with `, ", or ' (quotes stripped); multi-line quoted values supported (v21.7.0/v20.12.0); a leading export is ignored. There is NO variable expansionPASSWORD=${SECRET} is the literal string ${SECRET}, unlike dotenv-expand. This is the single most common migration surprise.

4. Task running (node --run) and watch mode (--watch)

node --run <script> (added v22.0.0, Stability 1.1 - Active development) runs a scripts entry from package.json — the in-core, faster alternative to npm run and a partial replacement for nodemon-style wrappers when combined with --watch.

node --run build              # runs package.json scripts.build
node --run test -- --watch    # everything after -- is forwarded to the script

Watch mode restarts the process on file changes — the in-core nodemon:

5. Filesystem & utility built-ins: fs.glob, util.styleText, structuredClone, navigator

6. Module compile cache — module.enableCompileCache() / NODE_COMPILE_CACHE

Persists V8’s code cache for CommonJS, ESM, and TypeScript modules to disk so subsequent process starts skip recompilation — a meaningful startup-time win for CLIs and serverless cold starts. Added v22.8.0; no longer experimental as of v25.4.0 (Stability 1.2 - Release Candidate).

// Best placed at the very top of the entry module, before other requires/imports
import { enableCompileCache } from 'node:module';
enableCompileCache(); // → { status, message?, directory? }

7. (Pointer) node:test — the built-in test runner

Node ships a full test runner (node --test, node:test, node:assert) that replaces Jest/Mocha for many projects. Deep coverage is deferred to the nodejs-test-runner sibling (mocking, code coverage, reporters, snapshot testing, watch integration). Listed here only so the “what’s built-in now” inventory is complete.

8. CLI app building with built-ins (util.parseArgs, readline/promises, signals, exit codes)

A small CLI no longer needs minimist/yargs for arg parsing or inquirer for simple prompts — util.parseArgs, node:readline/promises, and the process/tty globals cover the common cases. Combine with the shebang + node --run story from §4 (a scripts.cli entry, or a #!/usr/bin/env node file made executable) to ship a dependency-free tool.

Replaces (dep → built-in)

Third-party dep Built-in replacement Added / current stability Caveat
better-sqlite3 node:sqlite (DatabaseSync) v22.5.0 / 1.2 RC Synchronous-only; don’t block a hot request path
ws (client) global WebSocket v21 → default v22.0.0 / stable v22.4.0 Client only — no built-in server
dotenv --env-file / process.loadEnvFile() / util.parseEnv() v20.6.0 / Stable (v24.10.0/v22.21.0) No ${var} expansion
nodemon --watch / --watch-path / --watch-preserve-output v18.11+ (watch) Restarts whole process
npm run (speed) node --run v22.0.0 / 1.1 Skips pre/post scripts
chalk / colors util.styleText() v21.7.0/v20.12.0 / Stable Respects NO_COLOR/FORCE_COLOR
glob / fast-glob fs.glob / fs.globSync v22.0.0 / 1 Experimental No ! negation in exclude
lodash.cloneDeep structuredClone() v17 / Stable Can’t clone functions/methods
os.cpus().length navigator.hardwareConcurrency v21.0.0 / 1.1 navigator still Active-development
build-time compile caches module.enableCompileCache() / NODE_COMPILE_CACHE v22.8.0 / 1.2 RC Cache is Node-version-specific
jest / mocha node:test (see nodejs-test-runner) v18+ / Stable Deferred to sibling reference

Practical patterns

Anti-patterns

Troubleshooting

References