Code & DevelopmentCode & Development · 17 Apr 2026
Hono.js: The Web Framework You're Probably Not Using Yet — But Should Be
Express was built in 2010. Hono has zero dependencies, runs on any runtime, and processes 4-5x more requests. Here is why the JavaScript ecosystem is migrating.
Express was released in 2010. Back then, TypeScript wasn't a mainstream language, edge computing was science fiction, and Cloudflare Workers wasn't even on the horizon.
The JavaScript ecosystem of 2026 is a different world. We have Bun, Deno, Cloudflare Workers, AWS Lambda Edge — each with its own runtime quirks. And we have Hono: a framework built for this world.
2.8 million weekly downloads at the end of 2025 — 340% growth in one year. That's not hype. It's real adoption from teams that tested it and stayed.
12kB
zero external dependencies
100k+
requests per second
4-5×
faster on Node.js
What Hono is
Hono (炎) means flame in Japanese. It was created by Yusuke Wada with a specific goal: a web framework built on Web Standards that runs on any JavaScript runtime — no adapters, no workarounds.
The core difference from Express: Hono uses the native web platform APIs — Request, Response, fetch. Express uses Node.js's http module, which doesn't exist in other runtimes. That design decision is what makes Hono genuinely multi-runtime.
Getting started
Hono's API will feel familiar if you've used Express before. The difference shows up in the details — and the details are everything.
# For Node.js
npm create hono@latest my-app
# Select: nodejs
# For Cloudflare Workers
npm create cloudflare@latest my-app -- --template=hono
# For Bun
bun create hono my-appimport { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!'))
app.get('/json', (c) => c.json({ message: 'hello' }))
app.post('/echo', async (c) => {
const body = await c.req.json()
return c.json(body)
})
export default appThe c (context) object is the big difference from Express. Instead of two separate req and res parameters, Hono passes everything in a single typed context. c.req for input, c.json() / c.text() / c.html() for output.
Routing and route grouping
Hono's routing system uses a trie-based RegExpRouter — no linear loops. That's what guarantees performance even with hundreds of registered routes.
import { Hono } from 'hono'
const app = new Hono()
// Route parameters
app.get('/posts/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// Query strings
app.get('/search', (c) => {
const q = c.req.query('q')
const page = c.req.query('page') ?? '1'
return c.json({ q, page })
})
// Grouping with basePath
const api = new Hono().basePath('/api')
// Sub-routers per resource
const users = new Hono()
users.get('/', (c) => c.json([]))
users.post('/', async (c) => {
const body = await c.req.json()
return c.json(body, 201)
})
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
// Mount on the main app
api.route('/users', users)
export default apiMiddleware
Hono ships built-in middleware for the most common cases — no external packages required. CORS, logger, bearer auth, cache, compression — all available in the main package.
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { bearerAuth } from 'hono/bearer-auth'
import { cache } from 'hono/cache'
const app = new Hono()
// Global middleware
app.use('*', logger())
app.use('*', cors({
origin: ['https://mysite.com', 'http://localhost:3000'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))
// Route-level middleware
const token = 'my-secret-token'
app.use('/admin/*', bearerAuth({ token }))
// Cache on Cloudflare Workers
app.use('/static/*', cache({
cacheName: 'static-assets',
cacheControl: 'max-age=3600',
}))
// Custom middleware
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
})
app.get('/', (c) => c.text('OK'))
export default appZod validation — the real differentiator
The Zod integration is where Hono shows the difference in developer experience. Define the schema once and get automatic request validation, parsing and TypeScript type inference — all in one shot.
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
// User creation schema
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional(),
})
// Query params schema
const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
app.post(
'/users',
zValidator('json', createUserSchema),
(c) => {
const { name, email, age } = c.req.valid('json')
// name, email, age are fully typed here
// TypeScript knows exactly what they are
return c.json({ id: 1, name, email, age }, 201)
}
)
app.get(
'/users',
zValidator('query', paginationSchema),
(c) => {
const { page, limit } = c.req.valid('query')
// page and limit are already numbers, not strings
return c.json({ page, limit, total: 0, data: [] })
}
)
export default appWhen validation fails, Hono automatically returns a 400 Bad Request with the error details. No try/catch, no manual error logic.
RPC Client — end-to-end type safety
This is the feature that surprises people coming from Express the most. Hono ships a built-in RPC client that generates a typed client for calling API routes directly from the frontend — no tRPC, no codegen, no extra configuration.
// server.ts — export the route types
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
const route = app
.get('/api/posts', (c) => c.json([{ id: 1, title: 'Hello' }]))
.post(
'/api/posts',
zValidator('json', z.object({ title: z.string() })),
async (c) => {
const { title } = c.req.valid('json')
return c.json({ id: 2, title }, 201)
}
)
export type AppType = typeof route
export default app// client.ts — frontend with full type safety
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// TypeScript knows the return type of each route
const posts = await client.api.posts.$get()
const data = await posts.json()
// data is typed as { id: number; title: string }[]
// Compile error if the payload is wrong
const newPost = await client.api.posts.$post({
json: { title: 'New Post' }, // TypeScript validates here
})Native JSX and SSR
Hono supports JSX natively for server-side rendering — no React required, no bundler configuration. Useful for simple pages, transactional emails, or any dynamic HTML.
// app.tsx
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = ({ children }) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Site</title>
</head>
<body>{children}</body>
</html>
)
const PostCard: FC<{ title: string; summary: string }> = ({ title, summary }) => (
<article>
<h2>{title}</h2>
<p>{summary}</p>
</article>
)
app.get('/posts', (c) => {
const posts = [
{ title: 'Post 1', summary: 'Summary 1' },
{ title: 'Post 2', summary: 'Summary 2' },
]
return c.html(
<Layout>
<h1>Blog</h1>
{posts.map((p) => <PostCard {...p} />)}
</Layout>
)
})
export default appDeploy: same application, different runtimes
Portability is Hono's most important strategic advantage. The same application code runs on any runtime — only the adapter changes.
Node.js
import { serve } from '@hono/node-server' import app from './app' serve({ fetch: app.fetch, port: 3000, }) // package.json // "start": "node dist/server.js"
Cloudflare Workers
import app from './app' // Cloudflare Workers uses // the default export directly export default app // wrangler.toml // name = "my-app" // main = "src/index.ts"
For Bun: export default app directly — Bun recognizes the Hono object natively. Same for Deno and Vercel Edge. The correct adapter is selected by the runtime automatically.
Performance: what the benchmarks say
Express uses Node.js's http module with 13 years of accumulated synchronous middleware overhead. Hono is async-native, uses the Fetch API internally, and has an optimized radix trie router. Less work per request.
For most applications, the raw performance difference doesn't change production outcomes — database queries and external calls dominate response time. Hono's real advantage is in cases where the framework itself is on the critical path: API gateways, rate limiting, auth services.
When to use it and when not to
Hono isn't a silver bullet. The choice between it and Express depends on context.
✅ Use Hono when
Greenfield project Edge deployment (Cloudflare Workers) Need multi-runtime (Node + Bun + Deno) TypeScript is a priority Bundle size matters API gateway or performance proxy Small team, no Express legacy
⚠️ Stick with Express when
Legacy codebase with 50+ Express middleware Team already trained and productive in Express Depends on Express-specific libraries Migration cost exceeds the benefit No edge deployment plans
Express to Hono migrations typically take 2 to 4 weeks for typical APIs. The recommended strategy: route by route, starting with read-only endpoints.
What stays with you after reading this
Hono is built on Web Standards — Request, Response, fetch. That's why it runs on any runtime without adaptation.
The RegExpRouter is trie-based, not linear loops. That's what delivers 4-5x more performance than Express on Node.js.
The context c unifies req and res into a single typed object. Less boilerplate, more clarity.
Zod integration delivers validation, parsing and TypeScript inference from a single schema.
The RPC Client generates a typed client to call server routes directly from the frontend — no tRPC, no codegen.
The same Hono app runs on Node.js, Bun, Deno and Cloudflare Workers — only the adapter changes.
For new projects in 2026, especially with edge deployment: Hono is the correct default.
Express isn't going anywhere. It sustains millions of production applications and will keep getting maintained.
But the next project doesn't need to start in 2010.
Keep reading