The Hono Framework: A Hands-On Take on a Web-Standards Web Framework
- Problem: I needed an API layer running at the edge (Cloudflare Workers) that is small, has low cold start, yet stays end-to-end type-safe without an OpenAPI/codegen step.
- Solution: Hono — a framework built on Web Standards (real Request/Response), multi-runtime, with middleware in the core and RPC that shares types from server to client.
- Outcome: A few-KB bundle, millisecond cold start, types flowing straight from route to client. The trade-off: discipline around "optimistic" types and knowing when NOT to use it.
Short answer: Hono is a lightweight web framework (a few KB) built on Web Standards — it uses the same Request/Response objects as the browser's fetch() — so one piece of code runs on Cloudflare Workers, Bun, Deno, Node, and AWS Lambda. The reason it deserves a System Architect's attention is not that it is "faster than Express," but that it removes the runtime lock-in and lets types flow straight from the route to the client.
TL;DR (Executive Summary)
- The problem: This site's API layer (and a few internal microservices) runs on Cloudflare. I needed millisecond cold starts and a bundle small enough to deploy on Workers, while staying end-to-end type-safe without standing up OpenAPI and then running codegen.
- The solution: I rewrote that layer with Hono. Middleware (logger, CORS, JWT...) ships inside the core, no five-package
npm install. Its RPC mode shares types from server to client with a singleexport type.- The outcome: A bundle under ~15KB, single-digit-millisecond cold starts, near-zero code changes to switch runtimes. The price: Hono's types are "optimistic" — they trust you to keep your middleware in order, so the discipline is on you.
Redefining it: what is Hono from a systems-architecture view?
Most intros call Hono "a faster Express for the edge." That is easy to grasp but it ruins the real story.
The core difference is not speed — it is the compatibility layer. Express is written on Node's own req/res, an API more than a decade old that only exists inside Node. Hono is written on the Web Standards (WinterCG) Request/Response — the very objects you already use when you call fetch() in a browser.
The architectural consequence is large: because Hono only speaks the runtime's "common tongue," the same handler runs on Cloudflare Workers, Bun, Deno, Node, Vercel Edge, Fastly, and Lambda with almost no changes. You do not lock your architecture into one vendor. For someone who has to think about a system's 3-5 year lifespan, "not locked into a runtime" is an asset, not a nice-to-have.
Why I tried Hono (the real context)
This site deploys on Cloudflare Pages and has a few serverless functions (for example, a proxy endpoint that calls PageSpeed Insights so the API key never reaches the client). As the number of functions grew, two very real problems showed up:
- Cold start and CPU budget. Workers bill by CPU time and have hard limits. Drop a heavy Node framework in here and it either won't run or eats an uncomfortable cold start. I needed something that starts almost instantly.
- Types break at the network boundary. The frontend calls a function, but the request/response shapes have to be synced by hand. Change one endpoint's shape and you go fix the call sites manually — and TypeScript won't catch it for you.
Hono solves both at once, so I moved the API layer onto it.
Real code: routing and middleware
This is the part that felt "lighter" immediately. Middleware isn't a pile of separate installs — it lives in the core:
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors())
app.get('/api/health', (c) => c.json({ ok: true }))
app.get('/api/posts/:id', (c) => {
const id = c.req.param('id') // inferred as string
return c.json({ id })
})
export default app // on Workers, export default is all you need
No app.listen(), no server.js. On Cloudflare Workers, export default app is enough — because Hono speaks exactly the fetch-handler contract the runtime expects. Middleware runs in registration order: the "before next()" part of the first-registered middleware runs first, and its "after next()" part runs last. Understanding this matters when you stack auth, cache, and error handlers.
The best part: codegen-free type-safe RPC
This is the feature that kept me. You define a route with a validator, then export type that route:
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const route = app.post(
'/posts',
zValidator('json', z.object({ title: z.string(), body: z.string() })),
(c) => {
const data = c.req.valid('json') // already Zod-validated and typed
return c.json({ ok: true, id: 1 }, 201)
}
)
export type AppType = typeof route
On the client you create an hc (Hono Client) and pass it AppType:
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('https://api.example.com/')
const res = await client.posts.$post({
json: { title: 'Hono', body: 'tried it on Workers' },
})
if (res.ok) {
const data = await res.json() // data.id is inferred, no manual casting
}
No codegen step, no generated OpenAPI file to rebuild, no extra server. Types travel straight from the route definition to the call site through an ordinary import. Change the schema on the server and TypeScript goes red on the client immediately. This is what tRPC does, but Hono does it on plain HTTP/fetch instead of a bespoke protocol.
A practical comparison: Hono vs Express vs Elysia
The figures below come from public benchmarks and direct experience — absolute numbers depend on the environment, but the relative ranking is stable.
| Criterion | Express | Hono | Elysia |
|---|---|---|---|
| Bundle (minified) | ~572 KB | ~14–15 KB | ~15 KB |
| Runtimes supported | Mostly Node | Workers, Deno, Bun, Node, Lambda, Vercel | Bun-native |
| Core middleware | ≥5 separate packages | Built into core | Plugin system |
| Client type-safety | Practically none | RPC hc(), very good |
Eden Treaty, best |
| Throughput (on Bun) | Lowest | High | Highest |
| Cold start at the edge | Heavy | Single-digit ms | Not the focus |
Read the table like an architect: Elysia wins on raw throughput and the smoothness of its types, but it is tied tightly to Bun. Hono sits at the most balanced point — fast, small, good types, and it runs everywhere. Express is still fine for a traditional Node monolith, but at the edge it's the wrong tool.
Real-world gotchas: things few people warn you about
No framework is free. Here are three that cost me time:
1. Hono's types are "optimistic." Variables set via c.set() are typed at the app's global scope, so TypeScript lets you read c.var.db even on a route where the middleware that sets it never ran. It's green in the IDE but undefined at runtime.
app.use('/admin/*', async (c, next) => {
c.set('db', createDb(c.env))
await next()
})
app.get('/public', (c) => {
const db = c.var.db // no TS error, but this route skips the middleware -> undefined
})
This is a deliberate trade-off for a clean DX, but it demands discipline: which middleware sets which variable, and which route passes through it, is something you keep straight in your head or your folder structure.
2. ESM-only. Hono and @hono/node-server are ESM-only. If your project still has CommonJS, you'll hit module-resolution errors at runtime. Worth knowing before you drop it into an old codebase.
3. HonoX (the meta-framework) is still alpha. If you dream of a Next-style full-stack framework on Hono, HonoX is that direction — but minor versions can still break. For production today, the safe recipe is Hono for the API + a separate frontend, then track HonoX until it stabilizes.
One small tip from the docs that I confirmed: with many routes, compile first before opening the IDE, otherwise TypeScript's RPC inference can make the editor sluggish. And avoid c.notFound() with RPC — use c.json({...}, 404) so the status code makes it into the type.
When should you use Hono, and when not?
The short version: Hono is worth it for an edge API layer or microservice, and when you want end-to-end type safety without taking on tRPC/OpenAPI. Conversely, a stable Express monolith that just works shouldn't be migrated to chase a trend — that's negative value.
But this is a decision about cost and risk more than about technology — especially because you can't migrate from Express to Hono incrementally. I broke that part out into a dedicated decision post: Should you use Hono in production? A decision matrix and migration cost.
Frequently asked questions
Can Hono replace Express in every project?
No, and you shouldn't frame it that way. Hono shines in edge API layers and microservices — where small bundles and cold starts are real problems. A Node monolith running fine on the Express middleware ecosystem has no technical reason to be rebuilt. Choose the tool by the problem and the runtime, not by a pretty benchmark.
Is Hono's RPC the same as tRPC?
The experience is similar: both let types flow from server to client with no codegen. The difference is that tRPC builds a bespoke protocol on top of HTTP, while Hono keeps plain HTTP/fetch — the routes are still ordinary REST, and the hc() client is just a thin type layer over fetch. If you want endpoints you can still hit with curl like a normal API while staying type-safe in TypeScript, Hono's model fits better.
This post belongs to the cluster on architecture running at the edge: read Edge AI & Computer Vision in operations for the hardware/AI angle, or Supply chain Big Data with 100M+ records for the large-data architecture angle. If your business is weighing an edge API layer or an infrastructure rebuild, see the project stories or get in touch.