Skip to main content

The Decline of Express: What Developers Are Switching To

·PkgPulse Team

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

FrameworkReq/sec (simple JSON)BundleTypeScriptSchema 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

ScenarioPick
Replacing Express (REST API)Fastify
Edge/serverless deploymentHono
Multi-runtime (CF Workers + Node + Bun)Hono
Enterprise TypeScript, team structureNestJS
Bun-only, maximum performanceElysia
Existing Express app, low risk migrationStay on Express or migrate incrementally
Already on NestJSStay on NestJS (migration cost too high)
Simple API with 3-5 routesHono or Fastify

Compare backend framework package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.