Decline of Express: What Developers Are Switching 2026
Express.js holds a paradoxical position in the Node.js ecosystem: it is simultaneously the most-downloaded framework on npm and one of the least recommended for new projects. Download counts around 30 million per week make it look dominant. Survey data tells a different story — fewer than 5% of new projects started in 2026 choose Express as their framework. Understanding that gap is the entire point of this article.
TL;DR
Express isn't dying — it's already dead for new projects. Express (~30M weekly downloads) is still the most downloaded Node.js framework by a massive margin, but almost none of that is new adoption. Surveys show < 5% of new projects in 2026 choose Express. Fastify (~4M) gets most greenfield REST API projects. Hono (~1.5M) wins for edge/serverless. NestJS (~5M) dominates enterprise TypeScript APIs. The Express era is over; the question is which successor fits your use case.
Key Takeaways
- Express: ~30M weekly downloads — 90%+ from legacy; almost zero new projects
- NestJS: ~5M downloads — enterprise TypeScript standard, decorator-based
- Fastify: ~4M downloads — Express replacement for REST APIs, 2-3x faster
- Hono: ~1.5M downloads — edge/serverless, multi-runtime, tiny bundle
- Elysia: ~400K downloads — Bun-native, fastest HTTP performance benchmarks
The Download Paradox
Thirty million downloads per week sounds like a thriving project. In Express's case, it reflects the sheer mass of code already written on top of it rather than active new adoption. This distinction matters enormously when you're reading npm download charts and trying to understand ecosystem health.
The majority of Express downloads are transitive. When a package in your node_modules depends on Express — even tangentially — that counts as a download. Dozens of popular CLI tools, scaffolding utilities, and testing frameworks include Express as a dependency because they bundle development servers or mock APIs. Every npm install in projects using those tools increments Express's download counter, whether or not any developer ever wrote a single Express route.
Then there's the legacy project mass. The npm ecosystem has years of accumulated Express applications running in production, being maintained, getting dependency updates. Every time a team runs npm ci in their 2019 Express codebase, the counter goes up. This is organic usage in the sense that it represents real-world code, but it is not adoption — it is inertia.
The State of Node.js survey and the Stack Overflow Developer Survey paint a clearer picture. The 2025 Stack Overflow survey showed Express still ranking high in "used" responses but dropping sharply in "want to use" responses — the metric that actually predicts future adoption. Frameworks developers reach for when starting something new in 2025-2026: Fastify, Hono, NestJS. Express appears in the "maintained but not recommended" category alongside Koa and Restify.
How to read npm download data honestly: look at the trend line, not the absolute number. Express's weekly downloads have been essentially flat since 2021 while Fastify's have grown 3x and Hono's have grown from near-zero to 1.5M. A flat 30M while competitors grow at double-digit percentages annually tells you everything about where new projects are going.
The honest framing: Express is the jQuery of Node.js frameworks. Billions of existing lines of code depend on it, it still works fine, and it will receive security patches for years. But if you're starting a new API in 2026 and reaching for Express, you owe yourself 30 minutes of research first.
Why Express Fell Behind
Express was released in 2010 and hasn't had a major release (v5 was delayed for years — finally released in 2024). The problems that accumulated:
1. No TypeScript Support
// Express without types — the source of endless bugs
const express = require('express');
const app = express();
app.get('/user/:id', (req, res) => {
const userId = req.params.id; // string — fine
const user = req.body.user; // any — dangerous
const token = req.headers.token; // string | string[] | undefined — annoying
// TypeScript requires @types/express
// Even then, req.body is 'any' unless manually typed
res.json({ userId, user });
});
TypeScript's rise from a niche choice to the default for professional JavaScript development happened largely between 2019 and 2022. Express's @types/express package exists and is reasonably maintained, but the fundamental design of Express — where req.body is typed as any, where middleware mutates request objects without type-safe extension patterns, where there's no built-in way to define response shapes — resists TypeScript rather than embracing it. You spend significant effort fighting the type system instead of getting value from it. Teams building new TypeScript APIs found that Fastify's TypeBox integration or Hono's native generics made the TypeScript experience genuinely ergonomic in a way that Express never achieved.
2. Callback-First API (No Async/Await)
// Express error handling with async — infamous footgun
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err); // Must call next(err) or error is swallowed
// If you forget this line, the request hangs forever
}
});
// vs Fastify (proper async support)
fastify.get('/users/:id', async (request, reply) => {
const user = await User.findById(request.params.id);
return user; // Return value is the response — no res.json() needed
// If this throws, Fastify catches it and sends a 500 automatically
});
The async/await ergonomics problem in Express is not merely stylistic. It is a correctness issue. Forgetting to call next(err) inside a try/catch block means the request hangs indefinitely — no response, no error log, no indication anything went wrong. Production debugging sessions for this class of bug are notoriously painful. Express 5 addressed this by automatically forwarding rejected promises from async route handlers, but the fix came a decade after async/await became standard JavaScript. The ecosystem had already moved on. Fastify handled async correctly from its first major release in 2018, and that design decision attracted developers who were tired of the footgun.
3. No Schema Validation
// Express — manual validation or separate library
app.post('/users', (req, res) => {
const { name, email } = req.body;
// You must manually validate — forgot? User gets 500 or corrupt data
if (!name || !email) {
return res.status(400).json({ error: 'Missing fields' });
}
// ...
});
// Fastify — schema validation built in, automatic 400 on invalid input
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
},
response: {
200: { type: 'object', properties: { id: { type: 'string' } } },
},
},
}, async (request) => {
return createUser(request.body); // body is typed!
});
Schema validation in Express is always an afterthought — you add express-validator or joi or zod, wire it up as middleware, and maintain a separate validation layer from your route definitions. Fastify's JSON Schema validation is compiled to optimized validator functions at startup and runs before any route handler touches the request. The performance difference is measurable. The correctness difference is more important: when schema and handler are co-located in the same route definition, you cannot forget to validate. You cannot deploy a route that accepts arbitrary input because validation is not optional — it's structural.
The Middleware Ecosystem
Express's middleware ecosystem was once its greatest strength. The pattern of (req, res, next) => void was so simple that thousands of packages adopted it, and developers could assemble Express apps from community middleware without writing much infrastructure code. Helmet, morgan, cors, compression, body-parser, multer — the library of Express middleware covers virtually every common HTTP concern.
That ecosystem did not disappear when Express fell out of fashion for new projects. But its relevance shifted. Understanding which parts of the Express middleware world transfer cleanly to newer frameworks — and which require replacement — is practical knowledge for anyone doing migrations or evaluating alternatives.
Helmet (security headers) has a framework-agnostic mode and a Fastify plugin (@fastify/helmet) that wraps the same underlying logic. The migration is trivial. Morgan (request logging) has no direct Fastify equivalent by the same name, but Fastify's built-in logger: true option uses pino under the hood and provides structured JSON logging out of the box — which is strictly better for production observability than morgan's string-formatted logs. Most teams migrating to Fastify never miss morgan.
CORS handling transfers well. @fastify/cors mirrors the configuration API of the cors npm package closely enough that migrations require minimal changes. Hono has hono/cors middleware with a similar interface. Compression is available as @fastify/compress. Body parsing is built into Fastify and Hono — no separate middleware needed, no bodyParser.json() boilerplate.
The middleware that has no clean equivalent in Fastify or Hono is framework-specific Express middleware built on direct req/res object mutation. Some older packages attach properties directly to the Express request object in ways that don't transfer. This is where migration assessment matters: audit your middleware list early, identify anything doing direct object mutation, and check whether Fastify's plugin system (which uses a decoration pattern instead) covers the same functionality. In practice, most teams find the coverage is complete; the edge cases are usually very old packages that have been superseded anyway.
NestJS occupies a special position here: it runs on top of Express (or Fastify) as a platform adapter. Express middleware works unchanged in NestJS. This is one of NestJS's real advantages for teams with existing Express middleware investments — you get the structure and TypeScript ergonomics of NestJS while keeping your existing middleware layer.
The Migration Path Out of Express
Migrating an existing Express application is a different problem from choosing a framework for a new project. The calculus involves codebase size, team familiarity, risk tolerance, and how much of the application's logic is entangled with Express-specific patterns.
The first step in any Express migration is an honest assessment. How many routes exist? How many middleware layers? Are there custom req/res extensions that other parts of the codebase depend on? Is the application a simple REST API or does it use Express's template rendering features? A 20-route internal API is a weekend migration project. A 200-route customer-facing API with complex middleware chains is a multi-sprint effort requiring careful planning.
For medium-to-large Express apps, the strangler fig pattern is the safest migration strategy. Rather than rewriting the entire application at once, you introduce the new framework (Fastify or Hono) alongside Express and migrate routes incrementally. A reverse proxy (nginx, or even a thin Hono layer) routes requests to either the old Express handlers or new Fastify handlers based on path prefix. Over weeks or months, you migrate routes, verify behavior, and eventually decommission the Express layer. This approach keeps the application running throughout the migration with no big-bang cutover risk.
The Fastify-from-Express migration has a wrinkle worth knowing: @fastify/middie is a Fastify plugin that enables Express-compatible middleware in Fastify. This means you can run your existing (req, res, next) => void middleware in Fastify during a migration, progressively replacing middleware as you go rather than all at once.
For smaller applications, a direct rewrite is often cleaner. Fastify's route definition structure is similar enough to Express that a developer comfortable with Express can translate routes mechanically. The structural difference is that Fastify uses plugins and decorators rather than requiring middleware on specific routes. Plan for two to four hours per route file for non-trivial routes; simple CRUD routes migrate in minutes.
Common migration pitfalls: forgetting to add the .js extension to imports when moving to ESM, missing the fact that Fastify serializes responses through the schema (which filters out undeclared fields — a security feature, but it can silently drop data if your response schema is incomplete), and assuming that the Fastify plugin loading order matches Express middleware registration order (it doesn't — Fastify's plugin system has explicit encapsulation boundaries).
When choosing your migration target, team size and project type matter. Solo developers and small teams building REST APIs: Fastify. Multi-runtime deployments or edge functions: Hono. Large enterprise teams with strong opinions about project structure: NestJS. The express vs fastify comparison on PkgPulse has detailed package health metrics if you need to evaluate download trends and maintenance status side by side.
Express v5 in 2026: Too Little, Too Late?
Express v5 was released in late 2024 after years of delays, a changing maintainer team, and several false starts. The version number increment marks real improvements — but whether those improvements are enough to change the framework's trajectory is a separate question.
The headline changes in Express v5: async/await support in route handlers (rejected promises are automatically forwarded to error handlers, eliminating the next(err) footgun), updated path routing syntax with breaking changes to wildcard and named parameter handling, removal of deprecated APIs that accumulated over Express v4's decade-long run, and dependency updates that address long-standing security issues in older versions of the qs, path-to-regexp, and body-parser packages.
The async/await improvement is genuinely valuable. The most notorious Express footgun — forgetting to call next(err) in an async route handler, causing silent request hangs — is gone in v5. For teams maintaining existing Express codebases, this alone may be worth the upgrade effort.
What Express v5 does not add: TypeScript-first design, built-in schema validation, a plugin system with encapsulation, or meaningful performance improvements. The framework's architecture is the same. If the issues that drove developers to Fastify and Hono were specific to the async error handling behavior, v5 is a real fix. If the issues were deeper — the missing type safety, the lack of schema validation, the monolithic middleware model — v5 doesn't change the calculus.
The honest assessment for 2026: Express v5 is a worthwhile upgrade if you are maintaining an Express v4 application and have no current plans to migrate. The dependency security fixes alone justify it for production systems. It is not a reason to start new projects on Express instead of Fastify or Hono. The gap in TypeScript ergonomics and built-in validation that drives new project decisions is not closed by v5.
If you're deciding whether to upgrade a production v4 app to v5 or migrate to Fastify instead, consider the size of your application and your appetite for change. A small app with straightforward routes: migrate to Fastify and get the full benefit of a modern design. A large app with significant middleware investment: upgrade to v5 first (lower risk, immediate security and correctness improvements), then evaluate migration as a separate project.
The Replacements
Fastify (Best Express Replacement)
// Fastify — TypeScript, schema validation, 2-3x faster than Express
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
const fastify = Fastify({ logger: true })
.withTypeProvider<TypeBoxTypeProvider>();
// Register plugins
await fastify.register(import('@fastify/cors'), {
origin: ['https://app.example.com'],
});
await fastify.register(import('@fastify/jwt'), {
secret: process.env.JWT_SECRET!,
});
// Route with TypeBox schema — fully typed
fastify.post('/auth/login',
{
schema: {
body: Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String({ minLength: 8 }),
}),
response: {
200: Type.Object({ token: Type.String() }),
401: Type.Object({ error: Type.String() }),
},
},
},
async (request, reply) => {
const { email, password } = request.body; // Typed!
const user = await findUser(email);
if (!user || !verifyPassword(password, user.hash)) {
return reply.code(401).send({ error: 'Invalid credentials' });
}
const token = fastify.jwt.sign({ userId: user.id });
return { token };
}
);
await fastify.listen({ port: 3000, host: '0.0.0.0' });
Hono (Edge/Serverless)
// Hono — multi-runtime, runs on Cloudflare Workers, Bun, Node.js, Deno
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
app.post('/auth/login',
zValidator('json', z.object({
email: z.string().email(),
password: z.string().min(8),
})),
async (c) => {
const { email, password } = c.req.valid('json'); // Typed!
const user = await findUser(email);
if (!user || !verifyPassword(password, user.hash)) {
return c.json({ error: 'Invalid credentials' }, 401);
}
const token = signJwt({ userId: user.id });
return c.json({ token });
}
);
export default app; // Works on Cloudflare Workers, Bun, Node.js
NestJS (Enterprise TypeScript)
// NestJS — opinionated, Angular-inspired, decorator-based
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { IsEmail, IsString, MinLength } from 'class-validator';
class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() dto: LoginDto) {
// ValidationPipe auto-validates dto via class-validator
const token = await this.authService.login(dto.email, dto.password);
if (!token) throw new UnauthorizedException('Invalid credentials');
return { token };
}
}
NestJS: When Structure Is the Point
NestJS occupies a distinct category from Fastify and Hono. It is not primarily a performance play or an ergonomics improvement — it is an opinionated application framework that imposes an Angular-style architecture on Node.js backends. For teams where that structure is valuable, NestJS is genuinely excellent. For teams where it is unwanted overhead, it is the wrong tool regardless of its technical quality.
The core NestJS proposition: large teams building complex APIs benefit from having a framework enforce consistent patterns across modules, controllers, services, and guards. When everyone on a team must structure their code the same way, onboarding is faster, code reviews are more focused on logic than structure, and navigating an unfamiliar part of the codebase is easier. NestJS's module system, dependency injection container, and decorator-driven configuration create a codebase that reads consistently whether you're looking at the authentication module or the payment processing module.
The download numbers reflect this: NestJS at ~5M weekly downloads actually exceeds Fastify's ~4M, which surprised many observers who expected the more lightweight Fastify to dominate greenfield adoption. The explanation is enterprise adoption. Large companies building internal platforms, financial services APIs, and enterprise SaaS backends choose NestJS because the team structure it creates scales to ten or twenty developers working on the same codebase. Fastify is the better choice for a small team that wants full control; NestJS is the better choice for a team that wants constraints.
The TypeScript experience in NestJS is genuinely first-class in a way that Express never achieved. Controllers, DTOs, services, and guards are all strongly typed. The ValidationPipe with class-validator and class-transformer provides request validation with automatic deserialization and transformation. Injectable services are typed through the dependency injection container. The entire framework was designed TypeScript-first, and it shows.
The legitimate criticisms of NestJS: the learning curve is steeper than Fastify or Hono, the decorator syntax is unfamiliar to developers who haven't used Angular, the framework abstractions add overhead to operations that would be trivial in a simpler framework, and the bundle size (~150KB) is significantly larger than Fastify or Hono. The NestJS request/response cycle goes through more layers than a direct Fastify route, which is reflected in the benchmark numbers. If your API needs to handle sustained high-throughput loads on tight infrastructure budgets, NestJS's overhead is measurable. If your API handles typical web application traffic, the overhead is irrelevant and the organizational benefits outweigh it.
The practical guidance: if your team has five or more backend developers working on the same service, evaluate NestJS seriously. If you are a solo developer or a team of two, Fastify or Hono gives you TypeScript ergonomics and schema validation without the framework overhead.
Performance Comparison
| Framework | Req/sec (simple JSON) | Bundle | TypeScript | Schema Validation |
|---|---|---|---|---|
| Hono (Bun) | ~200,000 | ~12KB | ✅ First-class | ✅ Via Zod |
| Elysia (Bun) | ~180,000 | ~15KB | ✅ First-class | ✅ Built-in |
| Fastify | ~75,000 | ~30KB | ✅ Good | ✅ Built-in |
| Express | ~30,000 | ~60KB | ⚠️ Via @types | ❌ Manual |
| NestJS | ~25,000 | ~150KB | ✅ First-class | ✅ Via class-validator |
| Koa | ~40,000 | ~20KB | ⚠️ Via @types | ❌ Manual |
When to Choose
| Scenario | Pick |
|---|---|
| Replacing Express (REST API) | Fastify |
| Edge/serverless deployment | Hono |
| Multi-runtime (CF Workers + Node + Bun) | Hono |
| Enterprise TypeScript, team structure | NestJS |
| Bun-only, maximum performance | Elysia |
| Existing Express app, low risk migration | Stay on Express or migrate incrementally |
| Already on NestJS | Stay on NestJS (migration cost too high) |
| Simple API with 3-5 routes | Hono or Fastify |
Compare backend framework package health on PkgPulse.
See also: Fastify vs Hono, Express Is Dead, Long Live Express, and how to choose npm pnpm or yarn in 2026.
See the live comparison
View express vs. fastify on PkgPulse →