Hono.js: The Edge Framework Replacing Express 2026
The Edge Computing Context
Hono's rise can't be understood without understanding the edge computing shift in backend development. Cloudflare Workers, launched in 2018, proved that JavaScript could run in data centers globally with sub-millisecond cold starts and no server management. Vercel Edge Functions followed. Deno Deploy, AWS Lambda@Edge, and Fastly Compute@Edge extended the model. By 2024, deploying a serverless function that runs within 50ms of every user on the planet had become a realistic option for anyone with a credit card and 30 minutes.
The problem was that the existing Node.js ecosystem wasn't designed for this runtime. Express requires http.IncomingMessage and http.ServerResponse, which are Node.js-specific. Fastify uses Node.js streams under the hood. The entire ecosystem was built around the assumption that you'd have a persistent Node.js process. Cloudflare Workers don't have a persistent process — they use the Web Standards APIs (Request, Response, Headers, URL) that browsers also implement.
Hono was built from the ground up to target these Web Standards APIs. The result is a framework where the same application code runs on Node.js (via a thin adapter), Bun (natively fast), Cloudflare Workers (globally distributed), and Vercel Edge Functions (low-latency SSR) without modification. The app.fetch property is a standard (Request) => Promise<Response> function — exactly what all these runtimes expect.
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 metrics in the code block below tell part of the story. What they don't capture is the ecosystem alignment: Hono arrived at the right moment for TypeScript-native development. Express's type definitions are provided by @types/express from DefinitelyTyped, maintained separately from the framework, and have been a perennial source of type-safety gaps. Hono was built in TypeScript, and its router types propagate through the entire request handling chain. The c.req.valid() call after Zod validation isn't just a runtime assertion — TypeScript knows the exact type of the validated object and will error if you access a field that doesn't exist in the schema.
This TypeScript-first design is what makes the RPC mode possible. By typing the routes at definition time, Hono can generate a type-safe client that validates request bodies and response shapes at compile time, without any code generation step or separate schema definition language. It's the same type inference trick that tRPC uses, but lighter-weight because it builds on top of HTTP and JSON rather than a custom protocol.
Hono's npm download numbers tell the story of this alignment: 1K/week in 2022 when it was a Cloudflare Workers-first library, growing as the edge deployment model matured, and now at 20 million weekly downloads in 2026. The growth curve mirrors the adoption of TypeScript (now over 90% of new npm packages include types), Cloudflare Workers, and the shift toward edge-native architecture. Hono didn't change the ecosystem — it appeared exactly when the ecosystem was ready for it.
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
The side-by-side comparison below is the best argument for Hono's design. Notice that the Hono version has input validation built in via zValidator — the body is typed before the handler runs, and TypeScript will error if you access a field that wasn't declared in the schema. The Express version has untyped req.body, which is the source of an entire category of bugs and security vulnerabilities that Hono prevents structurally.
The other difference worth noting: Hono has no app.listen(). The framework exports a standard fetch handler, and the runtime (Node.js, Bun, Cloudflare Workers) is responsible for binding it to a port or an HTTP endpoint. This is what makes multi-runtime deployment possible without code changes — different adapters wrap the same fetch handler for their respective environments.
// ─── 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
The multi-runtime story is Hono's strongest selling point for teams that care about deployment flexibility. Most frameworks lock you into a runtime at the architecture level — converting an Express application to Cloudflare Workers requires significant rewriting because Express depends on Node.js APIs. Hono's Web Standards foundation means the same application can be tested locally in Node.js, deployed to Cloudflare Workers for global edge latency, and ported to Bun for maximum local performance, all without touching the application code.
The thin adapter layer at each deployment target is exactly what adapter-based architecture should look like: three to five lines that translate between the runtime's entry point and Hono's fetch handler. This gives you genuine portability without an abstraction layer that limits what you can build.
// 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.
Migrating from Express: What Changes
The conceptual migration from Express to Hono is straightforward for most route handlers: the middleware chain concept is the same, route parameters work identically, and the return-from-handler pattern is actually cleaner in Hono (return c.json(data) vs res.json(data) with no return needed).
The main architectural difference is how Hono handles the request/response lifecycle. Express handlers receive req and res as separate arguments and mutate res to send a response. Hono handlers receive a context object c and return a Response. The return is required — Hono uses it to know the handler is done. Forgetting to return is the most common Hono mistake for Express developers, and TypeScript will usually catch it because the return type would be undefined instead of Response.
Express middleware that calls next() to pass control to the next middleware maps to Hono's await next() pattern. Global middleware like CORS, authentication, and logging attach with app.use('*', middleware) — identical to Express's app.use(middleware). Error handling moves to app.onError((err, c) => c.json({ error: err.message }, 500)) rather than Express's four-argument error middleware.
Database connections and other stateful dependencies that you'd attach to Express's req object via middleware are handled in Hono via context variables (c.set('db', db) / c.get('db')), which are type-safe and don't pollute the global request type.
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.)
Hono for Full-Stack: Beyond APIs
Hono's feature set extends beyond JSON APIs. The hono/jsx module provides a React-compatible JSX runtime that runs server-side, enabling server-rendered HTML generation without a separate templating language. Hono Streaming support sends chunked responses incrementally, which is the foundation for streaming AI responses and large file transfers. The hono/html helper provides safe HTML generation.
For teams building full-stack applications without a separate frontend framework, Hono + JSX + HTMX or Hono + JSX + Alpine.js is a viable architecture. The simplicity of serving both the API and the HTML from a single Hono application, with the full TypeScript type safety and multi-runtime deployment benefits, is appealing for smaller teams and projects where the overhead of a separate Next.js or Nuxt application isn't justified.
For larger applications where a dedicated React/Next.js frontend makes sense, Hono's RPC mode provides the type-safe API bridge. The hc() client on the frontend shares types with the Hono routes on the backend without any code generation step. This is the same end-to-end type safety that tRPC provides, available for applications where tRPC's specific constraints (React-only, custom protocol) are limiting.
Built-In Middleware
Hono includes a comprehensive middleware set in the core package — zero additional dependencies required. This is in sharp contrast to Express, where the equivalent functionality requires installing five or more separate packages. The tradeoff is that Hono's built-in middleware is less customizable than specialized packages like cors or helmet, but for the vast majority of use cases, the built-in versions are sufficient and the zero-dependency story is worth maintaining.
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
When to Stay with Fastify or Express
Hono wins for new projects, greenfield APIs, and edge deployments. But Fastify has a specific strength that Hono doesn't match: JSON serialization performance. Fastify uses JSON Schema for both input validation and output serialization, and pre-compiles the serialization logic. For APIs that return large JSON payloads under high concurrency, Fastify's serialization throughput can be 2-3x faster than Hono's c.json(). If you're running a high-volume internal API on Node.js (not edge) and the bottleneck is JSON throughput, Fastify is worth considering.
Express remains appropriate for large codebases with significant investment in Express-specific middleware. The ecosystem of Express-specific plugins — Passport.js, multer, express-validator — is extensive and well-maintained. If your application heavily depends on middleware that doesn't have a Hono equivalent, the migration cost may not be worth the performance gain. Hono's plugin ecosystem is growing rapidly but hasn't yet matched Express's breadth for specialized use cases like file upload handling and complex session management.
Testing Hono Applications
Hono's Web Standards design makes unit testing routes dramatically simpler than Express. Because the application is just a fetch handler, you call app.request('/path', options) directly from tests and get back a standard Response — no supertest, no test server, no port binding required. This works out of the box in Vitest, the native Node.js test runner, and any other environment that supports fetch.
Mock your database and service layer dependencies at the context variable level — inject them via c.set() in a middleware that runs before your route handlers. In tests, pre-configure the mock before calling app.request(). This gives you fast, isolated tests that cover actual HTTP routing and response serialization, not just business logic.
The testing story extends to Cloudflare Workers. Cloudflare's @cloudflare/workers-types and miniflare provide a local Workers-compatible environment where Hono tests run with the same API surface. Writing a test suite that covers both Node.js and Workers environments is straightforward — the application code is identical, and the test setup is the only thing that changes per environment.
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 also: Express vs Hono and Fastify vs Hono, Best npm Packages for Edge Runtimes in 2026.
See the live comparison
View hono vs. express on PkgPulse →