Node.js in 2026: pick your stack
The 2021 Node stack was Node + npm + nvm and a third-party tool for everything else. By 2026 every layer has a serious challenger, and Node itself absorbed half of what used to be a separate dependency: test runner, watch mode, dotenv, native TypeScript. This is the snapshot of what to pick and why.
Pick a Node version
Node ships an LTS each October on even-numbered majors. As of May 2026:
| Version | Status | Use it when |
|---|---|---|
| 22 | Maintenance LTS (until Apr 2027) | An existing project that won’t budge |
| 24 | Active LTS (until Oct 2026, then Maintenance) | Default for new work |
| 26 | Current (becomes Active LTS Oct 2026) | Trying out features that haven’t crystallised |
Default to 24 LTS for new projects unless a dependency pins something older.
Version managers
| Tool | Language | Speed | Notes |
|---|---|---|---|
n |
shell | medium | Simple, sudo-required, Node-only |
nvm |
bash | slow shell startup | What every Stack Overflow answer assumes; sources a long bash script in every shell |
fnm |
Rust | fast | Drop-in nvm replacement; reads .nvmrc and .node-version |
volta |
Rust | fast | Project versions baked into package.json; release cadence has been quiet |
mise |
Rust | fast | Polyglot: Node + Python + Ruby + Go in one tool. asdf successor |
Homebrew (node@24) |
n/a | n/a | Fine for one global Node, painful for multi-version |
Pick by need:
- One global Node, no project pinning: Homebrew
- Multiple Node versions, only Node:
fnm - Multiple runtimes (Node + Python + Ruby + …):
mise
mise reads .tool-versions (asdf format) and .node-version, so it inherits whatever pin a project already has. See Mac development setup for the wider polyglot story.
Package managers
| Manager | Install | Speed | Disk |
|---|---|---|---|
| npm | bundled | baseline | duplicates per project |
| pnpm | corepack enable |
fast | content-addressable global store (saves GBs across projects) |
| yarn | corepack enable |
fast | PnP or node_modules. Yarn 4 (berry) is the modern line; Yarn 1.x classic is unmaintained |
| Bun | brew install bun |
fastest | global cache |
Corepack ships with Node and pins package-manager versions per project via the packageManager field in package.json. Enable it once globally; thereafter pnpm and yarn resolve to whatever each project pins, with no global-version drift across machines.
1 | corepack enable |
Runtime alternatives
Node is no longer the only thing that runs package.json projects:
- Bun (1.x in 2026): runtime, bundler, test runner, and package manager in one Zig binary. Imports npm packages directly, runs most Node code unchanged. Faster cold start and noticeably faster
bun install. Production-ready for backend services and CLIs; some native modules still flake on edge cases. - Deno (2.x): runs npm packages through
npm:specifiers and ships a stable Node compatibility layer. Tighter security model (explicit--allow-net,--allow-read). Worth picking for first-class TypeScript and a smaller install surface. - Node.js stays the safest default for libraries that depend on Node-specific behaviour. The 24 LTS feature gap with Bun/Deno has narrowed: test runner, watch mode, dotenv, and TypeScript all ship in stock Node now.
Modern Node.js (24 LTS)
What no longer needs a third-party dependency:
1 | # Watch mode (re-run on file change) |
A test, written against node:test:
1 | import { test } from 'node:test'; |
node --test finds it without config, tsconfig, or babel preset.
Modules: ESM is the default
package.json decides module resolution; "type": "module" makes the whole project ESM:
1 | { |
| Extension | Behavior |
|---|---|
.mjs |
Always ESM |
.cjs |
Always CommonJS |
.js |
Follows the nearest "type" field |
ESM features worth using day-to-day:
- Top-level
await(no IIFE wrapper around async startup) import.meta.urlandimport.meta.dirnamereplace__dirnamerequire(esm)works synchronously in Node 22+ behind a flag, default-on in 24
Cheat sheet
1 | # REPL |