Node.js

Modern Node.js App Architecture in 2026: A Practical Guide

By Ken Key May 11, 2026 5 min read Updated Oct 16, 2026

Modern Node.js App Architecture in 2026: A Practical Guide
The short answer

In 2026 I build Node apps on Node 24 LTS using native ESM, the built-in test runner, and direct TypeScript type stripping instead of a transpiler. Run tsc --noEmit in CI for type checking. Add a bundler only for serverless size limits, decorators, or path aliases.

Part of the complete app development guide.

On this page

Node.js has quietly grown up. The runtime that used to need a dozen build tools to feel modern now ships with most of what you need out of the box. If you’re maintaining an app that was scaffolded back in 2021 or 2022, a lot of that setup is now dead weight.

I’ve spent the last decade building and maintaining Node services, including plenty of work for small businesses around Long Island who don’t have a platform team to clean up after a bad scaffold. Here’s what I’d actually do in 2026.

Start with the runtime, not the framework

Node 24 is the current LTS line as of early 2026, with Node 22 still supported. Both give you native ESM, a stable built-in test runner, watch mode, and a real permission model. That means a chunk of your devDependencies can just go away.

The biggest practical shift is that you no longer need a transpiler just to run TypeScript. Node can strip types directly:

node --experimental-strip-types src/server.ts

Type stripping is stable enough to use in development and increasingly in production for services that don’t need decorators or complex transform pipelines. It only erases types. It does not type-check. You still run tsc --noEmit in CI to catch errors. That split (Node runs it, tsc checks it) is the pattern I’d recommend to anyone starting fresh.

When to still reach for a bundler

  • You’re shipping to a serverless platform with size or cold-start limits.
  • You use path aliases or decorators that type stripping doesn’t handle.
  • You need a single artifact for a container image.

Otherwise, skip it. Fewer moving parts means fewer 2 a.m. build failures.

Pick a framework by the shape of the problem

The “Express is dead” takes are mostly noise. Express 5 finally shipped stable, and for a straightforward JSON API it’s still fine. But the ecosystem around it has settled into clearer lanes:

  • Fastify if you want speed, schema-based validation, and a plugin system that scales past one team. This is my default for new APIs.
  • Hono if you care about running the same code on Node, Cloudflare Workers, Deno, and Bun. Great for edge deployments.
  • NestJS if you’re building something large with a team that benefits from opinionated structure and dependency injection.
  • Express if the app is small, the team knows it, and it already works.

Don’t pick a framework because a blog post ranked it first. Pick it because it matches how your team actually works. A two-person shop does not need NestJS’s ceremony, and a twenty-person platform team will regret raw Express.

Use the built-in test runner

Jest and Vitest are both good, but for a lot of Node services the native runner is now enough and removes a big dependency tree:

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

describe('invoice totals', () => {
 before(async () => {
 // seed test DB once
 });

 test('applies tax after discount', () => {
 const total = calculateTotal({ subtotal: 100, discount: 10, taxRate: 0.08 });
 assert.equal(total, 97.2);
 });
});

Run it with node --test. It supports mocking, coverage via --experimental-test-coverage, and watch mode. If you’re on Vitest because of its DX, that’s a legitimate reason to stay. If you’re on Jest only because that’s what the tutorial said, it’s worth a look at the native option.

Config, secrets, and the boring stuff that breaks apps

Two things cause more production incidents than bad code: config drift and unhandled rejections. Node 15+ made unhandled rejections crash the process by default, which is correct, but only if you’re logging them properly and your orchestrator restarts the container.

A minimal, honest config layer:

const required = ['DATABASE_URL', 'SESSION_SECRET', 'PORT'];

for (const key of required) {
 if (!process.env[key]) {
 throw new Error(`Missing required env var: ${key}`);
 }
}

export const config = {
 databaseUrl: process.env.DATABASE_URL,
 sessionSecret: process.env.SESSION_SECRET,
 port: Number(process.env.PORT),
 isProd: process.env.NODE_ENV === 'production',
};

Fail fast at boot. Do not let a missing variable surface as a confusing 500 three hours later. And keep secrets out of the repo. For local dev, a .env file that’s gitignored is fine. For production, use your host’s secret store.

The Long Island angle, briefly

Most of the businesses I work with here are not trying to build the next big SaaS. They’re running a shop, a clinic, an agency, or a small manufacturer and they need software that works and doesn’t need a full-time engineer to babysit. That changes the calculus. Fewer dependencies, boring choices, and a runtime that’s been stable for years beat chasing whatever shipped last week. If that’s the kind of work you need, my custom software engineering practice is built around exactly that.

What I’d actually tell you to do

If you’re starting a Node app in 2026:

  • Use Node 24 LTS.
  • Write ESM. The interop pain is mostly behind us.
  • Let Node strip types in dev, run tsc --noEmit in CI.
  • Reach for Fastify or Hono unless you have a specific reason not to.
  • Try node --test before adding a test framework.
  • Validate config at boot and crash loudly if it’s wrong.

None of this is exciting. That’s the point. The best Node setup in 2026 is the one you don’t have to think about.

FAQ

Do I still need TypeScript if Node can run it natively?

Node strips types, but it doesn’t check them. If you want type safety, keep TypeScript and run tsc --noEmit in CI or a pre-commit hook. Native execution just removes the build step, not the value of types.

Is Express still a reasonable choice?

Yes, especially for small APIs and existing codebases. Express 5 is stable and the ecosystem is enormous. If you’re starting something new and care about performance or schema validation, Fastify is usually the better default.

Should I migrate an old Node app to ESM?

Only if you have a reason. ESM is the future and the tooling has caught up, but a working CommonJS app that nobody’s touching doesn’t need a rewrite. Migrate when you’re already in there for another reason, or when a dependency forces your hand.

Have a project like this?

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