Modern Node.js in 2026: What Actually Changed and How to Upgrade

Node 22.6 added experimental type stripping and Node 24 makes it stable, so Node runs .ts files directly with no ts-node or Babel. It only strips erasable syntax, so enums and namespaces still need a build step. Keep tsc --noEmit in CI for real type checking.
Part of the complete web development guide.
On this page
- Native TypeScript support (finally, and mostly)
- When you still want a build step
- The built-in test runner is good enough now
- The permission model: run untrusted code more safely
- Things I’d stop doing in 2026
- A note on structure
- Should you upgrade right now?
- FAQ
- Do I still need TypeScript installed if Node strips types?
- Is the built-in test runner ready for production codebases?
- What Node version should a new project start on in 2026?
Most Node.js advice floating around is from 2019 and quietly wrong now. Node has absorbed a lot of the tooling we used to bolt on, and if you’re still running Babel, ts-node, and a pile of dev dependencies to do basic things, you’re doing extra work for no reason. Here’s what actually changed, what to rip out, and what to replace it with.
Native TypeScript support (finally, and mostly)
Node 22.6 shipped experimental type stripping via --experimental-strip-types, and by Node 24 it’s stable enough to use without the flag for most projects. That means Node reads your .ts files and strips the types at runtime. No build step, no loader, no ts-node.
There’s a catch, and it’s the one people trip on: type stripping only handles erasable syntax. Things like enums, namespaces, and parameter properties generate runtime code, so they aren’t supported. If you have a codebase full of enums, you’ll need to convert them or keep a build step.
// Runs directly with: node server.ts
// No tsconfig, no ts-node, no Babel.
interface User {
id: number;
email: string;
}
const users: User[] = [];
function addUser(email: string): User {
const user = { id: users.length + 1, email };
users.push(user);
return user;
}
console.log(addUser("test@example.com"));
What I’d actually do: keep tsc --noEmit running in CI so you still get real type checking, and let Node handle execution. You get fast startup and you don’t lose safety. That combination has replaced my old ts-node setup entirely.
When you still want a build step
- You’re deploying to an environment with an older Node version.
- You use enums or decorators heavily (NestJS projects, mainly).
- You need path aliases resolved at runtime, which type stripping doesn’t do.
The built-in test runner is good enough now
I resisted this for a while. Jest works, it’s familiar, and rewriting tests is tedious. But node:test plus node:assert covers what most backend projects need, and it removes a chunk of your dependency tree.
// users.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
test("adds a user with an incrementing id", () => {
const users = [];
users.push({ id: 1, email: "a@example.com" });
assert.equal(users[0].id, 1);
});
test("rejects duplicate emails", async () => {
const result = await checkDuplicate("a@example.com");
assert.equal(result, true);
});
Run it with node --test. Watch mode is node --test --watch. Coverage is node --test --experimental-test-coverage. No config file required.
Where Jest still wins: snapshot testing, heavy mocking, and frontend component tests. If you’re testing React, stay on Vitest or Jest. If you’re testing an Express API, the built-in runner is fine.
The permission model: run untrusted code more safely
Node’s permission model (--permission) lets you lock down what a process can touch. It’s still evolving, but it’s usable, and it’s a real improvement over “just run it and hope.”
# This process can read ./data and nothing else.
# No network, no writes, no child processes.
node --permission
--allow-fs-read=./data/*
--allow-fs-read=./config.json
server.js
Practical use case: build scripts, CI tasks, and anything that runs third-party code. It’s not a sandbox in the security-research sense, so don’t treat it as one. It’s a guardrail that catches accidental damage, not a determined attacker.
Things I’d stop doing in 2026
- Stop using
requirein new code. ESM is the default path now. Interop is fine for the packages that still need it. - Stop reaching for
dotenvfirst. Node supports--env-file=.envnatively. It reads the file and sets the variables. That’s the whole feature. - Stop ignoring
node:sqlite. There’s a built-in SQLite module now. For small apps and internal tools, it removes a native dependency that used to break on every Node upgrade. - Stop pinning to odd-numbered releases. They’re not LTS. Pick the even version and stay on it.
A note on structure
None of this matters if your project is a 4,000-line server.js. The tooling changes every couple of years; the structural stuff doesn’t. Routes in one place, business logic in another, database access behind a thin layer. I’ve written about this more broadly in my notes on custom web development, and the same principle holds whether you’re building in Node or anything else.
Should you upgrade right now?
If you’re on Node 18, yes, and soon. It’s past end of life. Node 20 is in maintenance. Node 22 and 24 are where active work is happening. The upgrade is usually a day of dependency bumps and a few broken test assertions, not a rewrite.
The honest answer on the new features: adopt them one at a time. Rip out ts-node first, see how it feels for a week. Then try the test runner on one small module. Trying to migrate everything at once is how people end up with a broken main branch and a weekend of regret.
FAQ
Do I still need TypeScript installed if Node strips types?
Yes, but only as a dev dependency for type checking. Node handles execution; tsc --noEmit handles catching your mistakes. You want both.
Is the built-in test runner ready for production codebases?
For backend and library code, yes. It’s stable, fast, and the API is small enough to learn in an afternoon. For frontend component testing, stick with Vitest.
What Node version should a new project start on in 2026?
Node 24 LTS. It has stable type stripping, the permission model, native env file support, and the built-in SQLite module. There’s no reason to start on anything older.


