Node.js spent the last two years quietly becoming a different runtime. By mid-2026 you can run TypeScript natively, write tests without a framework, ship ESM-only code without thinking, and pin permissions per process — all from the standard binary, with no transpilers, no ts-node, no Jest, and no third-party CLIs. The defaults finally match what the modern JavaScript ecosystem actually needs.

This is what's trending — what we've added to production, what we've removed, and what we're still watching as Bun and Deno keep applying pressure.

Node 22 Is the LTS Floor; Node 24 Is the Active Default

Node 22 entered Active LTS in late 2024 and is the maintenance floor we accept on any greenfield engagement. Node 24, released in 2025, is what we use for new builds. The cadence makes the version question simple: even-numbered releases get LTS, odd-numbered releases are current. By mid-2026 the right answer is "24 in dev, 22 in production if your platform hasn't caught up yet."

What 22 and 24 delivered together:

  • Native TypeScript support via --experimental-strip-types (stable in 24). You can run node app.ts directly. No ts-node. No tsx. No build step for scripts.
  • Built-in test runner via node --test. Includes assertions, mocks, coverage, watch mode, and parallel execution. Production-quality at this point.
  • Built-in WebSocket client. No more ws dependency for client-side WS.
  • Built-in fetch and FormData (stable since Node 18, but now finally matches browser semantics including streaming uploads).
  • Permission model via --permission. Limit what your process can read, write, or fork. Real security primitive, not a band-aid.
  • node --watch. nodemon is unnecessary in greenfield projects.
  • node --env-file. dotenv is unnecessary in greenfield projects.

The summary: a large chunk of the dependency graph that used to be standard in every Node project is now built in. Less to install, less to audit, less to break on the next upgrade.

TypeScript Without a Build Step

This is the biggest day-to-day change. Node 24's type stripping runs your TypeScript by erasing type annotations and executing the result — no tsc output, no source maps to ship, no build folder. For scripts, internal tools, CLIs, and small services, this is now how we run things:

node --experimental-strip-types ./scripts/migrate.ts

The constraint: it strips types, it does not check them. tsc --noEmit still runs in CI and pre-commit hooks. The runtime is fast; the type checker is the slow part, and we've separated them.

For application servers (Express, Fastify, Hono), most teams still build with tsc, tsup, or esbuild for production — bundling helps cold-start time on serverless and gives you a single artefact to ship. But the dev loop and most scripts now skip the build entirely.

The Built-in Test Runner Is Good Enough

For years the answer was "Jest, even though it's slow and ESM is awkward." Then Vitest became the answer. By mid-2026 there's a third answer: node --test. For most server-side libraries and small services, the built-in runner is sufficient — and you stop maintaining a test-runner dependency tree.

import { test } from "node:test";
import assert from "node:assert/strict";
import { sum } from "./math.ts";

test("sum adds two numbers", () => {
  assert.equal(sum(1, 2), 3);
});

Run with node --test. Coverage with node --test --experimental-test-coverage. Watch mode with node --test --watch. Parallelism is on by default. The reporter is configurable.

What we still reach for Vitest for: anything that needs jsdom, anything that benefits from Vite's transform pipeline, anything inside a React or Vue project where the test runner is shared with the build pipeline. The boundary is roughly: server libraries and CLI tools use node --test; web apps use Vitest. Jest is gone from new projects.

ESM-Only Is the New Default

By mid-2026, every library we write and most libraries we consume are ESM-only. CommonJS still exists for legacy compatibility, but greenfield code declares "type": "module" in package.json and never looks back. The interop story finally works:

  • require(esm) ships in Node 22+. CommonJS code can require ESM modules synchronously. The historic blocker for migration is gone.
  • Top-level await works in ESM modules without ceremony.
  • import.meta.resolve() and import.meta.dirname replace __dirname and require.resolve in ESM.
  • JSON imports via import data from "./data.json" with { type: "json" }.

The migration friction we used to feel — "this dep is CJS, this one is dual, this one breaks on import" — is mostly gone. If you have an inherited codebase still on CJS, the migration is finally worth doing.

The Permission Model Is a Real Security Primitive

This is the underrated feature of Node 22+. The --permission flag lets you start a Node process with explicit constraints on what it can read, write, spawn, or load:

node --permission \
     --allow-fs-read=/app/data \
     --allow-fs-write=/tmp \
     --allow-net=api.example.com \
     server.ts

The process cannot read files outside /app/data, cannot write outside /tmp, cannot make network calls to anything except api.example.com. This is enforced by the runtime, not by application code. If a vulnerable dependency tries to read /etc/passwd, it fails at the syscall layer.

We use this for any process that runs untrusted code — webhook handlers, sandboxed plugin runners, anything that evaluates user-supplied scripts. It's the closest thing Node has had to Deno's permission model, and by mid-2026 it's stable enough to recommend in production.

The Frameworks Have Consolidated

Express is fine for legacy apps. For new HTTP servers in 2026, the choice has narrowed:

  • Hono — fastest growing. Runs on Node, Bun, Deno, Cloudflare Workers, Vercel Edge. Tiny core, great TypeScript inference, real test coverage. We default to Hono for new APIs.
  • Fastify — still best in class for high-throughput Node-only APIs. Schema-driven, validates and serialises JSON faster than anything else. Use when you know you're running on Node and want the speed.
  • Elysia — Bun-first, but works on Node. End-to-end type safety with Eden client. Worth watching.

What we have stopped using for new builds: Express, Koa, NestJS for small services. NestJS still makes sense for large team monoliths with strong opinions about modules and DI — but for a microservice or a single-purpose API, it's over-architected in 2026.

Streams Got Better, Quietly

Web Streams (ReadableStream, WritableStream, TransformStream) are now first-class in Node. fetch().body returns a Web Stream. You can pipe between Web Streams and Node streams with .toWeb() and .fromWeb(). For new code we write Web Streams by default — the same code works in Node, Bun, Deno, Cloudflare Workers, and the browser.

The performance is genuinely better than Node streams for most workloads, and the API is simpler. If you've been writing fs.createReadStream(...).pipe(...) for years, the modern equivalent is shorter and portable across runtimes.

Bun and Deno Are Real Competition Now

For most production work we still default to Node — the ecosystem maturity, the deploy story, the platform support all favour it. But by mid-2026 we recommend Bun for specific cases (build tooling, CLIs, scripts) and Deno for others (edge functions, security-sensitive scripts, JSR-based libraries):

  • Bun — significantly faster cold start, built-in bundler, built-in package manager, built-in SQLite. We use it for bun run scripts and for projects where the speed of npm install becomes a CI bottleneck.
  • Deno — best-in-class security model, JSR registry for type-safe packages, first-class TypeScript. We use it for edge functions and short-lived scripts that need permission constraints stronger than Node's.

The good news for the ecosystem: the competition has forced Node to ship features it would have taken a decade to ship otherwise. Native TypeScript, the permission model, the built-in test runner — Bun and Deno had them first; Node now has them too.

What We Have Stopped Installing

The kill list from any new package.json we author:

  • nodemon — replaced by node --watch.
  • dotenv — replaced by node --env-file=.env.
  • ts-node / tsx — replaced by node --experimental-strip-types.
  • jest — replaced by node --test (server) or Vitest (web).
  • node-fetch — replaced by built-in fetch.
  • ws for client-side WebSocket — replaced by built-in WebSocket.
  • chalk for small CLIs — replaced by built-in util.styleText.
  • uuid for v4 — replaced by built-in crypto.randomUUID().

Each of these used to be standard. Each is no longer needed. The cumulative effect on node_modules size is real, and the upgrade-pain reduction is more real.

The Migration Order If You're Catching Up

For teams that froze at Node 16 or 18, the rollout we recommend:

  1. Upgrade to Node 22 LTS. If you're on 18, this is overdue.
  2. Drop node-fetch, nodemon, dotenv, uuid, and ts-node from new code. Use the built-ins.
  3. Try node --test on one library or service. Pilot it before committing.
  4. Migrate to ESM if you're still on CommonJS. Use require(esm) for incremental compatibility.
  5. Try --experimental-strip-types in dev for one project. Move the build pipeline last.
  6. Enable --permission on at least one high-risk service.

What's Coming Next

Three things to watch through late 2026:

  • Type checking in the runtime. The current type-stripping is non-checking. Microsoft's TypeScript-Go (the Go port of tsc) is fast enough to consider in-process type checking; pieces of this may land in Node in 2027.
  • WASI maturity. Running WASM modules with system access from Node is improving every release. Real use cases (image processing, audio codecs, Rust-in-Node) are now production-viable.
  • Better cold start. Snapshot startup (the --build-snapshot family) is becoming production-ready. Sub-100ms Lambda cold starts for substantial apps are within reach.

If you only do one thing after reading this: run node --version on your production servers. If it says anything less than 22, your team is paying interest on a debt that's about to come due.