Node.js Modern Batteries-Included Built-ins
Parent: JavaScript and Node.js · researched 2026-06-02T18:03:38.683Z· 19 sources · 7 concepts · skill nodejs-builtin-modules-modern
Between Node.js v20 and v26 (2024-2026) the runtime absorbed a wave of capabilities that
Overview
- Between Node.js v20 and v26 (2024-2026) the runtime absorbed a wave of capabilities that [source]
- historically required an npm dependency. A SQLite driver, a WebSocket client, .env [source]
- parsing, a task runner, file watching, glob matching, terminal colors, deep cloning, and a [source]
- V8 startup cache now all ship in core. The practical upshot: many small projects can drop [source]
- better-sqlite3, ws, dotenv, nodemon, chalk, glob, and lodash.cloneDeep [source]
- The catch - and the reason this reference exists - is **stability is per-feature and [source]
- recent**. Some of these are fully Stable (2), several are Release Candidate (1.2), and a [source]
- few are still Experimental (1) or "Active development" (1.1). Shipping an Experimental API [source]
- to production without pinning the Node version is the cardinal sin here. Every concept [source]
- below states its added-in version and current stability index explicitly; treat those [source]
- as the load-bearing facts, because an API that is RC today may change a method signature in [source]
- Scope boundaries (owned by sibling references in this family): Single-Executable [source]
- Applications, the --permission model, and native TypeScript stripping live in [source]
- nodejs-typescript-and-runtime-features; the deep node:test runner (mocking, [source]
- coverage, reporters, snapshots) lives in nodejs-test-runner - node:test is mentioned [source]
- here only as "it exists, it replaces Jest/Mocha for many projects, see the sibling"; [source]
- Bun/Deno/edge equivalents live in javascript-runtimes-deno-bun-edge. [source]
- A note on reading stability: index 2 = Stable; 1.2 = Release Candidate (API frozen, [source]
- shipping unflagged, final polish); 1.1 = Active development (unflagged but may change); [source]
- 1 = Experimental; 1.0 = Early development. Anything below 2 deserves a pinned [source]
- engines.node and a changelog read on upgrade. [source]
1. `node:sqlite` — a built-in synchronous SQLite driver
- Added in v22.5.0, unflagged since v23.4.0 / v22.13.0 (was behind [source]
- --experimental-sqlite), currently Stability 1.2 - Release Candidate. Available only [source]
- under the node: scheme. It is the in-core analogue of better-sqlite3: synchronous, [source]
- prepared-statement-centric, fast. [source]
- DatabaseSync(path[, options]) options: open (default true), readOnly, [source]
- enableForeignKeyConstraints (default true), allowExtension, timeout (busy timeout [source]
- ms), readBigInts, returnArrays, allowBareNamedParameters (default true), [source]
- allowUnknownNamedParameters. :memory: is an in-memory DB. [source]
- StatementSync (from db.prepare(sql)): get() → first row or undefined; [source]
- all() → array; run() → { changes, lastInsertRowid }; iterate() → row iterator. [source]
- Config methods: setReadBigInts(true) (read INTEGER as BigInt), [source]
- setReturnArrays(true), setAllowBareNamedParameters(true), [source]
- setAllowUnknownNamedParameters(true). Introspection: columns(), sourceSQL, [source]
- Parameter binding - three styles: anonymous ? (positional varargs), and named [source]
- :name / @name / $name (pass an object keyed by the prefixed name, e.g. [source]
- { ':id': 1 }; bare keys { id: 1 } work when allowBareNamedParameters is on). [source]
- db.aggregate(name, { start, step, result, inverse }) registers custom SQL aggregate [source]
- / window functions; backup(sourceDb, destPath, { rate, progress }) does an online [source]
- backup; db.loadExtension() requires allowExtension: true. constants (v23.5.0) [source]
- exposes SQLITE_CHANGESET_*, authorizer codes, etc. (serialize/deserialize landed later, [source]
- Type map: NULL↔null, INTEGER↔number|bigint, REAL↔number, TEXT↔string, [source]
- BLOB↔Uint8Array/TypedArray. [source]
2. The global `WebSocket` client (undici-backed)
- A spec-compliant, browser-compatible WebSocket is exposed on the global scope, backed [source]
- by undici. Timeline: experimental behind --experimental-websocket in v21, **on by [source]
- default in v22.0.0 (disable with --no-experimental-websocket), and no longer [source]
- experimental as of v22.4.0**. No import needed. [source]
- It replaces the ws package for client use only. Critically, there is **no [source]
- built-in WebSocket *server*** - to accept connections you still need ws (or another [source]
- library). The API is the WHATWG/browser WebSocket, not the ws EventEmitter API, so it [source]
- is portable to browsers but is not a drop-in for ws's .on('message') server-side [source]
3. Environment files — `--env-file`, `loadEnvFile()`, `util.parseEnv()`
- The in-core replacement for dotenv. Three surfaces: [source]
- --env-file=.env (CLI) - added v20.6.0, Stable since v24.10.0 / v22.21.0. [source]
- Loads the file into process.env before the app runs; Node-config vars like [source]
- NODE_OPTIONS are honored. Multiple --env-file flags stack (later overrides earlier). [source]
- Real process.env values take precedence over file values. **Throws if the file is [source]
- --env-file-if-exists=.env - added v22.9.0. Identical, but silently no-ops if the [source]
- file is absent (use for optional local overrides). [source]
- process.loadEnvFile([path]) - programmatic load (defaults to ./.env), added [source]
- v20.12.0/v21.7.0. util.parseEnv(content) (added v21.6.0 / v20.12.0) parses a [source]
- .env-format string and returns a plain object without mutating process.env. [source]
- .env parsing rules: KEY=value per line; text after # is a comment; values may be [source]
- quoted with ` `, ", or '` (quotes stripped); multi-line quoted values supported [source]
- (v21.7.0/v20.12.0); a leading export is ignored. There is NO variable expansion — [source]
- PASSWORD=${SECRET} is the literal string ${SECRET}, unlike dotenv-expand. This is the [source]
- single most common migration surprise. [source]
4. Task running (`node --run`) and watch mode (`--watch`)
- node --run <script> (added v22.0.0, Stability 1.1 - Active development) runs a [source]
- scripts entry from package.json - the in-core, faster alternative to npm run and a [source]
- partial replacement for nodemon-style wrappers when combined with --watch. [source]
- Sets NODE_RUN_SCRIPT_NAME (the script name) and NODE_RUN_PACKAGE_JSON_PATH [source]
- (resolved package.json path) in the child env; prepends node_modules/.bin to PATH. [source]
- Intentionally minimal: it does NOT run pre/post lifecycle scripts [source]
- (prebuild/postbuild are skipped), unlike npm run. This is the chief footgun when [source]
- migrating from npm - chained build steps silently stop running. It also doesn't read npm [source]
- config or run arbitrary shell features npm provides. [source]
- Watch mode restarts the process on file changes - the in-core nodemon: [source]
- --watch - restart on changes to the entry file and its imported module graph. [source]
- --watch-path=<dir> - watch explicit paths instead of the dependency graph (repeatable). [source]
- --watch-preserve-output - don't clear the terminal on restart (keep prior logs). [source]
- Combine with --run: node --run dev where scripts.dev is node --watch --env-file=.env server.js. [source]
5. Filesystem & utility built-ins: `fs.glob`, `util.styleText`, `structuredClone`, `navigator`
- fs.glob / fs.globSync / fsPromises.glob - added v22.0.0 (unflagged [source]
- v22.2.0), Stability 1 - Experimental (the least-mature item here). Replaces [source]
- glob / fast-glob. Options: cwd, exclude (a predicate (p) => boolean or [source]
- an array of glob patterns - note negation !pattern is not supported), [source]
- withFileTypes (return Dirent objects instead of path strings). [source]
- util.styleText(format, text[, options]) - terminal ANSI styling; replaces chalk [source]
- / colors / kleur. Added v21.7.0 / v20.12.0, since stabilized to 2 - Stable. [source]
- format is a style name or an array of them (e.g. ['bold', 'red']); colors and [source]
- modifiers like bold, italic, underline, dim, bgGreen are supported. It honors [source]
- NO_COLOR / FORCE_COLOR and falls back to tty.hasColors() auto-detection; [source]
- pass { stream: process.stdout } so it decides based on the actual output target. [source]
- structuredClone(value) - global, added v17.0.0 (precisely v17.6.0 / v16.15.0), [source]
- Stable (WHATWG standard). Deep-clones via the structured-clone algorithm (handles [source]
- Map/Set/Date/ArrayBuffer/typed arrays/circular refs), replacing [source]
- lodash.cloneDeep for clonable data. Caveat: it **cannot clone functions, DOM-less [source]
- class prototypes (methods are dropped → plain objects), or symbols** - it throws [source]
- DataCloneError on functions. [source]
- navigator - global Web-interop object, added v21.0.0, **Stability 1.1 - Active [source]
- development** (disable with --no-experimental-global-navigator). [source]
- navigator.hardwareConcurrency (v21.0.0) returns the logical-CPU count - a cleaner [source]
- replacement for os.cpus().length when sizing worker pools; navigator.userAgent [source]
- (v21.1.0) is Node.js/<major>; navigator.language / navigator.languages (v21.2.0) [source]
- report the runtime locale. [source]
6. Module compile cache — `module.enableCompileCache()` / `NODE_COMPILE_CACHE`
- Persists V8's code cache for CommonJS, ESM, and TypeScript modules to disk so [source]
- subsequent process starts skip recompilation - a meaningful startup-time win for CLIs and [source]
- serverless cold starts. Added v22.8.0; no longer experimental as of v25.4.0 [source]
- (Stability 1.2 - Release Candidate). [source]
- module.enableCompileCache([directory]) returns { status, message?, directory? } [source]
- where status is one of module.constants.compileCacheStatus: ENABLED, [source]
- ALREADY_ENABLED, FAILED (with message), or DISABLED (when [source]
- NODE_DISABLE_COMPILE_CACHE=1). Without an argument it uses the NODE_COMPILE_CACHE [source]
- env var, else os.tmpdir()/node-compile-cache. [source]
- module.getCompileCacheDir() returns the active cache dir (or undefined); [source]
- module.flushCompileCache() (v22.10.0+) writes accumulated cache to disk immediately [source]
- rather than waiting for process exit - useful before spawning children that should reuse it. [source]
- NODE_COMPILE_CACHE=<dir> enables it without code changes (set it once, no [source]
- enableCompileCache() call needed). NODE_COMPILE_CACHE_PORTABLE=1 (or [source]
- { portable: true }) lets the cache survive the project being moved. Caches are [source]
- Node-version-specific; first run is slightly slower (cache is generated then), and [source]
- code coverage is slightly less precise on deserialized functions. [source]
7. (Pointer) `node:test` — the built-in test runner
- Node ships a full test runner (node --test, node:test, node:assert) that replaces [source]
- Jest/Mocha for many projects. Deep coverage is deferred to the nodejs-test-runner [source]
- sibling (mocking, code coverage, reporters, snapshot testing, watch integration). Listed [source]
- here only so the "what's built-in now" inventory is complete. [source]
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 [source]
- prompts - util.parseArgs, node:readline/promises, and the process/tty globals cover [source]
- the common cases. Combine with the shebang + node --run story from §4 (a scripts.cli [source]
- entry, or a #!/usr/bin/env node file made executable) to ship a dependency-free tool. [source]
- util.parseArgs([config]) - added v18.3.0 / v16.17.0, Stability 2 - Stable [source]
- since v20.0.0; the in-core replacement for minimist / yargs (for non-trivial [source]
- arg parsing). config.options keys are long names; each value is `{ type: 'string' | [source]
- 'boolean' (required), short, multiple, default }. Parser flags: args` (defaults to [source]
- process.argv minus execPath+filename), strict (default true - throws on unknown [source]
- args / type mismatch), allowPositionals (default false when strict), allowNegative [source]
- (--no-foo sets a boolean false; added v22.4.0 / v20.16.0), and tokens (return a [source]
- parsed-token stream to extend behavior). Returns { values, positionals, tokens? }. [source]
- Defaults landed in v18.11.0 / v16.19.0. [source]
- node:readline/promises - added v17.0.0, Stability 2 - Stable since [source]
- v24.0.0 / v22.17.0; the async/await prompt API (replaces inquirer/prompts for [source]
- simple questions). createInterface({ input, output }) then await rl.question(query) [source]
- resolves to the typed line; rl.close() when done. question accepts { signal } (e.g. [source]
- AbortSignal.timeout(10_000)) to cancel a hung prompt. [source]
- Line processing via async iteration - the interface is an async iterable [source]
- (Symbol.asyncIterator, added v11.4.0 / v10.16.0), so a CLI can stream stdin or a file [source]
- line-by-line; break/return/throw out of the loop auto-calls rl.close(). Use [source]
- crlfDelay: Infinity to treat \r\n as one break. (For perf-critical bulk reads the [source]
- 'line' event is faster than iteration.) [source]
- process.argv / argv0 / execPath - process.argv is `[execPath, scriptPath, [source]
- ...args]; parseArgs already strips the first two by default, so reach for raw argv` [source]
- only when you need the script path or a passthrough tail. process.argv0 (v6.4.0) is the [source]
- original argv[0] even if argv was rewritten; process.execPath (v0.1.100) is the [source]
- resolved node binary path (handy for re-spawning the same runtime). [source]
- Exit codes - prefer process.exitCode over process.exit(). Set [source]
- process.exitCode = 1 and let the event loop drain; calling process.exit() terminates [source]
- synchronously and can truncate async stdout/stderr writes (they may span multiple [source]
- ticks), so a usage message printed right before exit(1) can be lost. Convention: 0 = [source]
- success, non-zero = failure; an unhandled SIGINT/SIGTERM exits with 128 + signal. [source]
- Signal handling for graceful shutdown - process.on('SIGINT', …) (Ctrl-C, all [source]
- platforms) and process.on('SIGTERM', …) (all except Windows) let a long-running CLI flush [source]
- buffers, close handles, then set process.exitCode and return. Installing a listener [source]
- overrides the default 128 + n exit, so set the code yourself. [source]
- Detect interactive vs piped, and color. process.stdout.isTTY (the stream.isTTY [source]
- flag, v0.5.8) / tty.isatty(fd) tell you whether output is a terminal or a pipe - gate [source]
- spinners/prompts/ANSI on it. For the colors themselves, use util.styleText (see §5) [source]
- rather than hand-rolling escapes; it already honors NO_COLOR/FORCE_COLOR and [source]
- tty.hasColors() (added v11.13.0 / v10.16.0) when passed { stream }. [source]
Practical patterns
- Gate on the Node version. Set "engines": { "node": ">=22.13" } (or whatever each [source]
- feature you use requires) in package.json and verify in CI. These APIs simply don't [source]
- exist on older runtimes, and Experimental/RC ones can change between minors. [source]
- Dependency-free local dev script. "dev": "node --watch --env-file-if-exists=.env.local --env-file=.env src/server.js", [source]
- launched via node --run dev - replaces the nodemon + dotenv-cli + npm run stack [source]
- with zero node_modules. [source]
- node:sqlite for embedded/test data. Use :memory: databases as fast, disposable [source]
- fixtures in tests; prepared statements are reusable - prepare() once at module scope, [source]
- run/get/all many times. [source]
- Right-size worker pools with navigator.hardwareConcurrency instead of importing [source]
- os - const pool = Math.max(1, navigator.hardwareConcurrency - 1). [source]
- Compile cache for CLIs/cold starts. Either call enableCompileCache() as the first [source]
- line of the entry file, or ship a launcher that sets NODE_COMPILE_CACHE; for processes [source]
- that spawn workers, flushCompileCache() then pass NODE_COMPILE_CACHE down so children [source]
- util.styleText({ stream }) so color is decided by the real output target (pipe vs [source]
- TTY), and let NO_COLOR work for free instead of hand-rolling a supportsColor check. [source]
Anti-patterns
- Shipping an Experimental/RC API to prod without pinning Node. fs.glob (1), [source]
- navigator/node --run (1.1), and node:sqlite/compile-cache (1.2) can change. Pin [source]
- engines.node and read the changelog on upgrade - don't assume "it's in core so it's [source]
- Treating the global WebSocket as a server. It's a client. Reaching for it to [source]
- accept connections fails; you still need ws server-side. [source]
- Blocking the event loop with node:sqlite. It is synchronous by design; a large [source]
- query or write inside an HTTP handler stalls every other request. Keep heavy SQLite work [source]
- off the main thread (worker thread) or out of hot paths. [source]
- Expecting ${VAR} expansion in --env-file. Core .env parsing does no [source]
- interpolation; configs that relied on dotenv-expand break silently. [source]
- Assuming node --run runs pre/post scripts. Migrating a prebuild/postbuild [source]
- chain to node --run build silently drops those steps. [source]
- Using structuredClone on objects with methods/functions. Methods are lost (you get [source]
- a plain object) and a function value throws DataCloneError - it clones data, not [source]
- Negation patterns in fs.glob exclude. '!keep.js' is not supported; use a [source]
- predicate function or a positive pattern set. [source]
Troubleshooting
- ERR_UNKNOWN_BUILTIN_MODULE / "Cannot find module 'node:sqlite'" → the Node version [source]
- predates v22.5.0, or it's v22.5.0-v22.12 and you didn't pass --experimental-sqlite [source]
- (unflagged only from v23.4.0/v22.13.0). Check node -v. [source]
- WebSocket is not defined → Node < v22 (need --experimental-websocket on v21), or [source]
- someone passed --no-experimental-websocket. On supported versions it's a global; no [source]
- --env-file throws on missing file → expected; switch to --env-file-if-exists for [source]
- optional files. If a variable is "ignored," remember real process.env overrides file [source]
- values, and that there is no ${} expansion. [source]
- node --run "command not found" for a tool that works under npm run → it's a [source]
- pre/post script or relies on npm-injected env/PATH behavior node --run doesn't [source]
- replicate. Run the underlying binary directly (it is on node_modules/.bin). [source]
- fs.glob results differ from the glob package → core glob has its own semantics [source]
- (no ! negation in exclude; withFileTypes returns Dirents). It's also Experimental, [source]
- so behavior can shift between minors - pin Node. [source]
- styleText prints raw escape codes / no color → output isn't a TTY (auto-detection), [source]
- or NO_COLOR is set, or FORCE_COLOR is needed; pass { stream } and check the env vars. [source]
- Compile cache "doesn't help" / status: DISABLED → NODE_DISABLE_COMPILE_CACHE=1 is [source]
- set, or you upgraded Node (caches are version-specific and regenerate), or the cache dir [source]
- isn't writable (status: FAILED, check .message). First run is always slower. [source]
- navigator is undefined → Node < v21, or --no-experimental-global-navigator was [source]
- passed; it's still Stability 1.1. [source]
References
- Node.js - node:sqlite (DatabaseSync/StatementSync, params, aggregate, backup, stability): https://nodejs.org/api/sqlite.html [source]
- Node.js - Node 22 release announcement (node:sqlite, node --run, WebSocket default, fs.glob): https://nodejs.org/en/blog/announcements/v22-release-announce [source]
- Node.js - global WebSocket (history: v21 flag → v22.0.0 default → v22.4.0 stable): https://nodejs.org/api/globals.html [source]
- Node.js - Native WebSocket Client guide (undici-backed, client-only): https://nodejs.org/learn/getting-started/websocket [source]
- Node.js v21.0.0 release (initial --experimental-websocket): https://github.com/nodejs/node/releases/tag/v21.0.0 [source]
- Node.js - CLI (--env-file, --env-file-if-exists, --run, --watch, --watch-path, --watch-preserve-output, NODE_COMPILE_CACHE): https://nodejs.org/api/cli.html [source]
- Node.js - util.parseEnv & util.styleText (formats, NO_COLOR/FORCE_COLOR, versions): https://nodejs.org/api/util.html [source]
- Node.js - util.parseArgs (options type/short/multiple/default, positionals, strict, allowNegative, tokens; Stable since v20.0.0): https://nodejs.org/api/util.html#utilparseargsconfig [source]
- Node.js - node:readline / node:readline/promises (createInterface, rl.question, async line iteration; promises Stable v24.0.0/v22.17.0): https://nodejs.org/api/readline.html [source]
- Node.js - tty module (tty.isatty, stream.isTTY, writeStream.hasColors/getColorDepth): https://nodejs.org/api/tty.html [source]
- Node.js - process (argv/argv0/execPath, exitCode vs exit(), stdin/stdout/stderr, SIGINT/SIGTERM signal events): https://nodejs.org/api/process.html [source]
- Node.js - process.loadEnvFile(): https://nodejs.org/api/process.html#processloadenvfilepath [source]
- Node.js - Node 22.10.0 release (node --run env vars, flushCompileCache): https://nodejs.org/en/blog/release/v22.10.0 [source]
- Node.js - fs.glob / globSync / fsPromises.glob (cwd/exclude/withFileTypes, Stability 1): https://nodejs.org/api/fs.html [source]
- Node.js - global objects: structuredClone, navigator (hardwareConcurrency/userAgent/language, Stability 1.1): https://nodejs.org/api/globals.html [source]
- Node.js - module.enableCompileCache / getCompileCacheDir / flushCompileCache (v22.8.0, stable v25.4.0): https://nodejs.org/api/module.html [source]
- Node.js - V8 code caching background ("Code caching for JavaScript developers"): https://v8.dev/blog/code-caching-for-devs [source]
- Node.js - GitHub CHANGELOG (per-version "added/unflagged/stabilized" notes for all of the above): https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V22.md [source]
- Node.js - node:test runner (deferred - deep coverage in the nodejs-test-runner sibling): https://nodejs.org/api/test.html [source]
Children
- node:sqlite (DatabaseSync/StatementSync, params, aggregate, backup) — replaces better-sqlite3 (frontier)
- Global undici-backed WebSocket client (client-only) — replaces ws for client use (frontier)
- Environment files: --env-file / --env-file-if-exists / process.loadEnvFile() / util.parseEnv() — replaces dotenv (frontier)
- Task running & watch: node --run + --watch / --watch-path / --watch-preserve-output — replaces npm run / nodemon (frontier)
- Filesystem & utils: fs.glob/globSync, util.styleText(), structuredClone, navigator — replace glob/chalk/lodash.cloneDeep/os.cpus (frontier)
- Module compile cache: module.enableCompileCache() / NODE_COMPILE_CACHE — startup performance (frontier)
- CLI app building with built-ins (util.parseArgs, readline/promises, signals, exit codes) (frontier)
Frontier under this node: CLI app building with built-ins (util.parseArgs, readline/promises, signals, exit codes), Environment files: --env-file / --env-file-if-exists / process.loadEnvFile() / util.parseEnv() — replaces dotenv, Filesystem & utils: fs.glob/globSync, util.styleText(), structuredClone, navigator — replace glob/chalk/lodash.cloneDeep/os.cpus, Global undici-backed WebSocket client (client-only) — replaces ws for client use, Module compile cache: module.enableCompileCache() / NODE_COMPILE_CACHE — startup performance, Task running & watch: node --run + --watch / --watch-path / --watch-preserve-output — replaces npm run / nodemon, node:sqlite (DatabaseSync/StatementSync, params, aggregate, backup) — replaces better-sqlite3