Hono.js in 2026: The Edge Framework That's Replacing Express
·PkgPulse Team
TL;DR
Hono is now the default choice for new Node.js/edge API projects. It runs on every JavaScript runtime with zero code changes, has first-class TypeScript with end-to-end type safety (RPC mode), ships ~14KB, and benchmarks at 3-5x faster than Express. The migration from Express is straightforward — the API is familiar. The edge-first design means Cloudflare Workers, Bun, Vercel Edge, and Deno all work natively. If you're starting a new backend project in 2026 and don't have a specific reason to use Express or Fastify, start with Hono.
Key Takeaways
- Multi-runtime: same code runs on Node.js, Bun, Deno, Cloudflare Workers, Vercel Edge, AWS Lambda
- Performance: ~3-5x faster than Express; comparable to Fastify in benchmarks
- Bundle size: ~14KB total — Express is ~200KB with its common deps
- TypeScript RPC:
hc()client gives end-to-end type safety like tRPC, without the overhead - Download growth: 1K/week (2022) → 5M/week (2024) → 20M/week (2026)
Why Hono Won
The JavaScript backend landscape (2026):
Express (2010):
→ 35M weekly downloads (mostly legacy, slow growth)
→ Node.js only
→ CommonJS first, ESM support added later
→ No TypeScript types (DefinitelyTyped separate)
→ ~200KB with common middleware
→ Middleware ecosystem is huge but unmaintained
→ 9+ years between v4 and v5
Fastify (2016):
→ 10M weekly downloads
→ Node.js only (some Bun support)
→ Fast (schema-based serialization)
→ TypeScript support
→ Good for Node.js-only projects
Hono (2022):
→ 20M weekly downloads and growing fast
→ Every JavaScript runtime
→ TypeScript-first from day one
→ Web Standards API (Request/Response)
→ 14KB total
→ RPC mode for type-safe clients
→ Built-in middleware: auth, CORS, rate limit, logger, etc.
The shift happened when:
1. Cloudflare Workers went mainstream (Node.js APIs don't work there)
2. Bun adoption grew (wanted fast + standard)
3. Edge deployment became standard (Vercel Edge, Cloudflare Workers)
4. TypeScript became the default (not an add-on)
Getting Started: Hono vs Express Side by Side
// ─── Express ───
import express from 'express';
const app = express();
app.use(express.json());
app.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.post('/users', async (req, res) => {
const { name, email } = req.body; // untyped
const user = await createUser({ name, email });
res.status(201).json(user);
});
app.listen(3000);
// ─── Hono ───
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 user = await getUser(c.req.param('id'));
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user);
});
app.post('/users',
zValidator('json', z.object({
name: z.string().min(1),
email: z.string().email(),
})),
async (c) => {
const { name, email } = c.req.valid('json'); // fully typed!
const user = await createUser({ name, email });
return c.json(user, 201);
}
);
export default app;
// Note: no app.listen() — Hono exports a fetch handler
// The runtime (Node.js/Bun/Cloudflare) handles the server
Multi-Runtime: Same Code Everywhere
// One Hono app, multiple deployment targets:
// app.ts (your actual app — same for all runtimes):
import { Hono } from 'hono';
export const app = new Hono()
.get('/health', (c) => c.json({ status: 'ok' }))
.get('/users', async (c) => {
const users = await db.user.findMany();
return c.json(users);
});
export type AppType = typeof app;
// ─── Node.js ───
// index.node.ts:
import { serve } from '@hono/node-server';
import { app } from './app';
serve({ fetch: app.fetch, port: 3000 });
// ─── Bun ───
// index.bun.ts:
import { app } from './app';
export default app; // Bun uses default export with .fetch
// ─── Cloudflare Workers ───
// worker.ts:
import { app } from './app';
export default app; // Workers use default export with .fetch
// ─── Vercel Edge ───
// api/[[...route]].ts:
import { handle } from 'hono/vercel';
import { app } from '../../app';
export const GET = handle(app);
export const POST = handle(app);
// ─── AWS Lambda ───
// lambda.ts:
import { handle } from 'hono/aws-lambda';
import { app } from './app';
export const handler = handle(app);
// The same app.ts runs everywhere with a thin adapter layer.
// Move from Node.js to Cloudflare Workers: change 3 lines.
Hono RPC: End-to-End Type Safety
// Hono's RPC feature — type-safe client like tRPC but lighter:
// server/routes/users.ts:
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const UserRoutes = new Hono()
.get('/:id', async (c) => {
const user = await getUser(c.req.param('id'));
return c.json({ user });
})
.post('/',
zValidator('json', z.object({ name: z.string(), email: z.string().email() })),
async (c) => {
const data = c.req.valid('json');
const user = await createUser(data);
return c.json({ user }, 201);
}
);
export type UserRoutesType = typeof UserRoutes;
// server/index.ts:
import { Hono } from 'hono';
const app = new Hono().route('/users', UserRoutes);
export type AppType = typeof app;
// client.ts (frontend or another service):
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
// Fully typed — no manual type declarations:
const response = await client.users[':id'].$get({ param: { id: '123' } });
const { user } = await response.json();
// user: inferred from server return type ✓
const createResponse = await client.users.$post({
json: { name: 'Alice', email: 'alice@example.com' }
});
// TypeScript validates the request body against the Zod schema ✓
// TypeScript error if wrong body shape ✓
// vs tRPC: no separate router definition, no adapter, lighter setup
// Works with any HTTP client (not just hc — curl, fetch, etc.)
Built-In Middleware
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { rateLimiter } from 'hono/rate-limiter';
import { bearerAuth } from 'hono/bearer-auth';
import { compress } from 'hono/compress';
import { secureHeaders } from 'hono/secure-headers';
import { etag } from 'hono/etag';
const app = new Hono();
// All built in — zero additional dependencies:
app.use('*', logger());
app.use('*', cors({ origin: ['https://myapp.com'] }));
app.use('*', secureHeaders()); // CSP, HSTS, X-Frame-Options, etc.
app.use('*', compress()); // gzip/brotli
app.use('*', etag()); // ETag caching
// Rate limiting:
app.use('/api/*', rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100,
keyGenerator: (c) => c.req.header('x-forwarded-for') ?? 'anonymous',
}));
// Auth:
app.use('/admin/*', bearerAuth({ token: process.env.ADMIN_TOKEN! }));
// JWT auth:
import { jwt } from 'hono/jwt';
app.use('/protected/*', jwt({ secret: process.env.JWT_SECRET! }));
// Compare to Express:
// cors → npm install cors
// helmet → npm install helmet (security headers)
// morgan → npm install morgan (logging)
// express-rate-limit → npm install express-rate-limit
// jsonwebtoken → npm install jsonwebtoken
// 5+ packages vs 0 additional packages with Hono
Performance
Benchmark: HTTP requests/second (Node.js 22, M2 MacBook Pro)
Route: GET /users/:id with JSON response
Framework req/s p99 latency
─────────────────────────────────────────
Hono 98,200 1.2ms
Fastify 91,400 1.4ms
Express 28,600 4.8ms
Koa 31,200 4.2ms
Cloudflare Workers (edge, same Hono app):
120,000+ 0.8ms
Why Hono is fast:
→ Web Standards (Request/Response) = no translation layer
→ Trie-based router (O(log n) route matching vs Express's O(n))
→ No legacy CommonJS overhead
→ Small middleware stack — each middleware adds minimal overhead
→ JIT-friendly code patterns
Compare Hono, Express, Fastify, and other Node.js framework download trends at PkgPulse.
See the live comparison
View hono vs. express on PkgPulse →