Express vs Hono in 2026: Legacy vs Modern Node.js
TL;DR
Express has 20x more downloads but Hono is the better choice for new projects. Express (35M+ weekly downloads) won through ubiquity and age. Hono (1.8M) wins on TypeScript, performance, and modern runtime support. For greenfield APIs in 2026, Hono is the correct default. For maintaining Express apps, stay — migration isn't worth it for stable codebases.
Key Takeaways
- Express: ~35M weekly downloads — Hono: ~1.8M (npm, March 2026)
- Hono is 4-5x faster on Node.js; much faster on Bun
- Express has no TypeScript — Hono is TypeScript-first with request type inference
- Hono runs on Cloudflare Workers — Express cannot (Node.js APIs only)
- Hono is Express-compatible — familiar API, easy learning curve
The Gap in Context
Express's download dominance is a trailing indicator. It powers millions of production Node.js applications — most of which aren't being rewritten. The install numbers reflect maintenance of existing systems, not new project adoption.
New project starts tell a different story. In 2026, Hono, Fastify, and Elysia have captured the majority of net-new backend Node.js applications. Express is still started for quick prototypes by developers who know it, but the ecosystem is clearly shifting.
API Design Comparison
// Express — callback-based, no types
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', async (req, res) => {
// req.params.id is string | undefined — no type safety
const user = await db.user.findUnique({
where: { id: req.params.id }
});
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.post('/users', async (req, res) => {
// req.body is any — needs manual validation
const { name, email } = req.body;
if (!name || !email) return res.status(400).json({ error: 'Missing fields' });
const user = await db.user.create({ data: { name, email } });
res.status(201).json(user);
});
app.listen(3000);
// Hono — TypeScript-first, web standards
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
app.get('/users/:id', async (c) => {
const id = c.req.param('id'); // typed as string
const user = await db.user.findUnique({ where: { id } });
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user); // response type inferred
});
app.post('/users',
zValidator('json', z.object({
name: z.string().min(1),
email: z.string().email(),
})),
async (c) => {
const body = c.req.valid('json'); // { name: string, email: string }
const user = await db.user.create({ data: body });
return c.json(user, 201);
}
);
export default app; // Works on Cloudflare Workers
The type safety difference is meaningful in large codebases. req.body being any in Express leads to undetected type errors. Hono's c.req.valid('json') returns the validated, typed body.
Middleware Comparison
// Express middleware (familiar but untyped)
app.use((req, res, next) => {
req.user = verifyJWT(req.headers.authorization);
next();
});
// Accessing custom properties requires declaration merging in TS
// This is a common pain point
// Hono middleware with typed context
import { createMiddleware } from 'hono/factory';
const authMiddleware = createMiddleware<{
Variables: { user: { id: string; email: string } }
}>(async (c, next) => {
const token = c.req.header('Authorization')?.slice(7);
if (!token) return c.json({ error: 'Unauthorized' }, 401);
const user = verifyJWT(token);
c.set('user', user); // typed
await next();
});
// Usage
app.use('/api/*', authMiddleware);
app.get('/api/me', (c) => {
const user = c.get('user'); // { id: string; email: string }
return c.json(user);
});
Hono's middleware typing is a significant DX improvement — you get full autocomplete on c.get('user') without declaration merging hacks.
Performance Benchmarks (Node.js)
| Framework | Req/s | Latency (p99) |
|---|---|---|
| Hono | ~350K | ~2ms |
| Fastify | ~230K | ~3ms |
| Express | ~80K | ~8ms |
Hono is ~4x faster than Express on equivalent hardware. For most applications, this doesn't matter — database queries dominate response times. But for high-throughput APIs (auth services, rate limiting, proxy layers), it's significant.
The @hono/node-server Migration Path
For Express developers, Hono's API is intentionally familiar:
// Express patterns that map directly to Hono
// app.get → app.get ✅
// app.post, app.put, app.delete → same ✅
// app.use(middleware) → app.use(middleware) ✅
// res.json() → c.json() ✅
// req.params → c.req.param() ✅
// req.query → c.req.query() ✅
// req.body → c.req.json() (async) ✅
// Deploy on Node.js
import { serve } from '@hono/node-server';
serve(app, (info) => {
console.log(`Listening on port ${info.port}`);
});
When to Choose
Choose Express when:
- Maintaining an existing Express codebase (don't rewrite stable code)
- You need a very specific Express middleware with no Hono equivalent
- Your team only knows Express and learning overhead is a constraint
- You're building a quick proof-of-concept where familiarity wins
Choose Hono when:
- Starting any new Node.js backend project in 2026
- Deploying to Cloudflare Workers or other edge runtimes
- TypeScript type safety matters to your team
- Performance headroom is valuable
- You want built-in Zod validation without boilerplate
Compare Express and Hono package health on PkgPulse.
See the live comparison
View express vs. hono on PkgPulse →