"Is Node Still The Right Choice In 2026?"
I get this question almost weekly. It used to be asked anxiously — "should we be on Bun by now?" — and now it's asked the way you'd ask if Postgres is still a good database. The answer, in both cases, is: yes, more than ever, and the reasons are boring in the best way.
Node.js spent the last few years quietly closing the gaps that made people consider switching. Native TypeScript. Stable built-in test runner. Permission model. ESM as the default story. WebStreams in core. A --watch flag that just works. None of these are headline features, but together they shifted Node from "the JavaScript runtime that grew up at startups" into "the default backend runtime, full stop."
Worth talking about what changed, what's stable, what's still flagged, and where Bun and Deno actually fit.
TypeScript Without A Build Step
This is the biggest shift in years. Node 22.6 introduced --experimental-strip-types for running .ts files directly, and 22.7 enabled it by default with an experimental warning. Type stripping was unflagged in Node 22.18 and 24.3 — you can node app.ts on current LTS and it works (no enums or namespaces; use --experimental-transform-types for those, or stick with tsx).
// app.ts
const greet = (name: string): string => `hello ${name}`;
console.log(greet('world'));
node app.ts
# hello world
What it actually does: Node strips the type annotations and runs the resulting JavaScript. It does not type-check. It does not understand enum, namespace, or other TypeScript features that emit JavaScript at runtime — those still need tsc or a transpiler. For 90% of TypeScript code (interfaces, type aliases, parameter types), strip-only execution is enough.
This kills a category of tooling overnight. tsx, ts-node, swc-node — for many use cases, you no longer need them. CI scripts, small CLIs, internal tools, even production servers if you're disciplined about keeping types simple, can run .ts files directly.
You still want tsc --noEmit in CI for type checking. Node strips, it doesn't validate. That's the contract.
A Stable, Built-In Test Runner
node:test has been stable since Node 20 and the experience keeps improving. There is no longer a real reason to install Jest, Mocha, or Vitest for a typical Node service.
import { test, describe, before } from 'node:test';
import assert from 'node:assert/strict';
import { add } from './math.ts';
describe('add', () => {
before(() => console.log('warming up'));
test('sums two numbers', () => {
assert.equal(add(2, 3), 5);
});
test('handles negatives', () => {
assert.equal(add(-1, 1), 0);
});
});
node --test --watch
Plus the test reporter is decent, parallel execution is built in, and snapshot/mock helpers are included (mock, mock.method, mock.timers). It's not as feature-rich as Vitest, but for the test suites I see in real services, it's plenty — and the install footprint is zero.
ESM Won (Mostly) And CJS Still Ships
By 2026, ESM is unambiguously the default story. New projects start with "type": "module" in package.json. Most popular libraries publish dual builds. Top-level await works without a flag. Dynamic import() is everywhere.
CommonJS is still supported and will be for a long time — there's too much production code on it for a hard cutover. The interop story has stabilized too: you can require() an ESM module from CJS via require(esm) (stabilized in recent Node), and import a CJS module from ESM has worked for a while.
The practical advice: for new code, start with ESM. For existing CJS codebases, don't migrate just to migrate — there's no functional payoff most of the time. The interop is good enough that mixed codebases are fine.
The Permission Model
--permission (formerly experimental --experimental-permission) lets you run Node with explicit allowlists for filesystem reads, filesystem writes, child processes, and network access:
node --permission \
--allow-fs-read=/app \
--allow-fs-write=/tmp \
--allow-net=api.stripe.com \
server.ts
If a dependency tries to read /etc/passwd or shell out to curl, it gets blocked with a clear error. This isn't a sandbox at the level of Deno's permission model, but it's a real, useful supply-chain mitigation that didn't exist a few years ago. Worth turning on for production deployments where you can characterize the IO surface.
Treat it as defense in depth, not a license to install random packages.
What's In Core That Used To Be A Library
A short list of "you don't need a dependency for this anymore" wins, mostly accumulated since Node 18:
fetch,Request,Response,Headers,FormData. No morenode-fetch.AbortControllerandAbortSignal. Native cancellation, supported byfetch, streams, and most modern APIs.- WebStreams (
ReadableStream,WritableStream,TransformStream). Cross-runtime stream API, interops cleanly with Node's classic streams. node --watch. No morenodemonfor most cases. Restarts on file change, plays nicely with the test runner.node --env-file=.env. No moredotenvfor the simple case.- WebSocket client. Built-in
WebSocketglobal is unflagged in modern Node 22+. structuredClone,crypto.randomUUID,crypto.subtle. Standard web crypto on the server.
The collective effect: a fresh Node project's package.json is significantly smaller in 2026 than it was in 2022. That's a good direction.
The LTS Story Is Sane Again
Node 24 became Active LTS in October 2025, so by 2026 it's the LTS line for new production work; Node 22 is in Maintenance LTS and remains a fine choice if you're already on it. Node 18 hit EOL in April 2025 and should not be running anywhere outside of legacy maintenance.
Pick LTS for production. Historically only even-numbered majors became LTS — the project announced in 2025 that the schedule is moving toward promoting one yearly major (released in April) to LTS in October regardless of even/odd. Either way, the predictability is a quiet win compared to a decade ago.
So Where Do Bun And Deno Fit?
Honest take, having tried both in production:
- Bun. Genuinely fast at startup, very fast for some I/O patterns, ships with a bundler and test runner, npm-compatible. Where it shines: scripts, internal tooling, small services where startup latency matters. Where it doesn't: large existing Node codebases with deep native-module dependencies, where compatibility is still a moving target. The compatibility gap is closing fast but isn't zero.
- Deno. Best-in-class permission model, native TypeScript with full type checking, very pleasant DX. Deno Deploy gives you a credible edge platform. Where it shines: greenfield TypeScript projects, edge-deployed APIs, security-conscious workloads. Where it doesn't: anywhere you need the depth of the npm ecosystem and Node-specific module behavior, although
npm:specifiers have closed a lot of the gap. - Node.js. The default. Largest ecosystem. Most production deployments. Most stack overflow answers. Most hires who've used it. Most observability tooling that works out of the box.
The honest framing: pick Bun when startup speed or DX is the dominant constraint. Pick Deno when permissions and TypeScript are first-class needs. Pick Node when you want the boring default that the rest of your stack already assumes — which is most of the time.
What's Still Flagged Or Evolving (Be Honest)
A few things that aren't quite there yet, even in 2026:
- Some advanced TypeScript syntax in strip mode. Decorators, full enum semantics, namespace merging — still need a transpiler.
- The permission model. Useful, but not a full sandbox. Some APIs aren't covered.
- Worker threads ergonomics. The API works but feels older than the rest of the platform.
Don't believe anyone (including me) who tells you the platform is finished. It's mature, not done.
A One-Sentence Mental Model
Node.js in 2026 is what people wished it was in 2018 — TypeScript runs natively, the test runner is built in, ESM is the default, the standard library covers most of what you used to install, and "is Node still relevant" is a question people only ask if they haven't looked in two years.






