The Decline of Express: What Developers Are Switching To
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
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 });
});
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
});
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!
});
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 };
}
}
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 the live comparison
View express vs. fastify on PkgPulse →