Development Tips

Node.js in 2026: What Actually Changed and What Still Matters

By Ken Key Mar 21, 2025 4 min read Updated Oct 16, 2026

Node.js in 2026: What Actually Changed and What Still Matters
The short answer

Node 22 is the current LTS and 24 is close behind, but the real shift is what ships built in. I use node:test instead of Jest for server code, and the permission model now locks down filesystem and child process access at startup. Fewer dependencies, less config.

Part of the complete web development guide.

On this page

Node.js has changed more in the last three years than it did in the previous eight. A lot of the advice floating around is from 2019 and will actively hurt you now. Here’s what I actually run in production and what I’ve stopped doing.

The runtime itself got boring, which is good

Node 22 is the current LTS line, and 24 is rolling toward it. The interesting part isn’t the version number. It’s that a bunch of things we used to install as dependencies are now just there.

node:test replaced Jest for most of my projects

I ran Jest on everything for years. The config alone was a part-time job. The built-in test runner is fast, has no dependencies, and works with coverage out of the box.

import { test, describe } from 'node:test';
import assert from 'node:assert/strict';

describe('invoice totals', () => {
 test('applies tax after discount', () => {
 const total = calculateTotal({ subtotal: 100, discount: 10, taxRate: 0.0875 });
 assert.equal(total, 97.88);
 });

 test('rejects negative subtotals', () => {
 assert.throws(() => calculateTotal({ subtotal: -1, discount: 0, taxRate: 0 }));
 });
});

Run it with node --test and you’re done. I still reach for Vitest when I need browser-mode testing or heavy mocking, but for server code the built-in runner wins on setup time alone.

The permission model is real now

This one matters if you’re running anything you didn’t write. You can lock down filesystem, child process, and worker access at startup:

node --permission 
 --allow-fs-read=/var/app/data 
 --allow-fs-write=/var/app/tmp 
 server.js

Without a flag, the process can’t touch the disk or spawn anything. I’ve started using this on build scripts and third-party CLIs. It catches supply chain nonsense before it becomes an incident, not after.

Native TypeScript, mostly

Node strips TypeScript types directly now. No ts-node, no build step for scripts:

node --experimental-strip-types migrate.ts

Read that flag carefully. It strips types, it does not type-check. Your types are documentation at runtime, nothing more. I still run tsc --noEmit in CI because a typo that gets stripped is a typo that ships.

One gotcha: type stripping doesn’t handle enums or namespaces, because those emit runtime code. If you’re migrating a codebase full of enums, you’ll want --experimental-transform-types or you’ll want to refactor them to union types. I’d refactor.

What I stopped doing

Stop reaching for a framework by default

Express is still fine. Fastify is faster and has a better plugin model. But a shocking number of APIs don’t need either. Node’s built-in HTTP server plus a router is maybe 60 lines and has zero dependencies to audit.

import { createServer } from 'node:http';

const routes = {
 'GET /health': (req, res) => {
 res.writeHead(200, { 'content-type': 'application/json' });
 res.end(JSON.stringify({ ok: true }));
 },
};

createServer((req, res) => {
 const key = `${req.method} ${new URL(req.url, 'http://x').pathname}`;
 const handler = routes[key];
 if (!handler) {
 res.writeHead(404).end();
 return;
 }
 handler(req, res);
}).listen(3000);

That’s not the right call for a 200-route application. It is the right call for a webhook receiver or an internal service. I’ve watched teams pull in a framework, a validation library, and an ORM to serve four endpoints. That’s maintenance debt you volunteered for.

Stop using callbacks for anything sequential

I still see callback pyramids in code reviews. Promises and async/await have been standard for a decade. There is no performance argument for callbacks in application code.

Stop ignoring the event loop

The single most common Node performance problem I get called about is a synchronous operation blocking everything. fs.readFileSync in a request handler. A giant JSON.parse on every call. A regex with catastrophic backtracking.

If you take one thing from this post: instrument the event loop. Node exposes it directly.

import { monitorEventLoopDelay } from 'node:perf_hooks';

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
 console.log('p99 lag (ms):', (h.percentile(99) / 1e6).toFixed(2));
 h.reset();
}, 10000);

If that p99 number creeps past 50ms under load, you have a blocking call somewhere. Find it before your users do.

Where Node still isn’t the answer

I like Node. I also don’t use it for everything.

  • CPU-heavy work. Image processing, video transcoding, heavy crypto. Use a worker thread, or honestly, use a different language. Python with native extensions or Go will beat a single-threaded JS loop every time.
  • Long-running batch jobs with tight memory budgets. The garbage collector is good, not magic.
  • Anything where you need real threads with shared memory and no ceremony. Worker threads exist but the ergonomics are still rough.

For the vast majority of web applications (APIs, server-rendered sites, real-time features, background queues) Node is a genuinely good choice and it’s gotten better, not worse.

A note on the local angle

Most of my work is for businesses in Commack, Huntington, and around Long Island, and the Node story there is the same as anywhere: small teams maintaining more surface area than they can support. Every dependency you don’t add is a dependency you don’t have to patch at 11pm. That’s why I lean on built-ins now. If you’re weighing a rebuild versus patching what you have, my custom web development work is mostly that decision, made honestly.

FAQ

Should I upgrade to the latest Node LTS right now?

Yes, if you’re on anything below Node 20. Node 18 is end-of-life and no longer gets security patches. Test on the new LTS in a branch first, then move. The breaking changes between recent LTS lines have been small for most applications.

Do I still need a build step for TypeScript?

For scripts and small services, no. Node strips types natively. For anything with enums, decorators, or path aliases, keep your build step and run tsc --noEmit in CI regardless. Type stripping does not catch type errors.

Is Express dead?

No, but it’s not the default choice anymore. Fastify has better performance and a cleaner plugin system, and for small services the built-in HTTP module is enough. Pick based on your route count and team size, not on what a 2018 tutorial said.

Have a project like this?

I do custom web development for Long Island businesses. If you need this done right, let’s talk.