hono
Hono is a small web framework built on the Web Standards Request/Response API instead of Node-specific objects, which is why the same app runs unchanged on Cloudflare Workers, Deno, Bun, Fastly Compute, AWS Lambda and Node.js. You get Express-style routing, built-in middleware, zero dependencies, and unusually good TypeScript inference including a typed RPC client for your frontend.
The best default for edge and multi-runtime APIs right now, and light enough that picking it costs little. Pick Express or Fastify instead only when you know you are Node-forever and need their ecosystems.
Use it if
- You deploy to edge runtimes like Cloudflare Workers where Express simply does not run
- You want one codebase that can move between Bun, Deno, Node and serverless without a rewrite
- You share types between backend and frontend and want the hono/client RPC to give you a fully typed fetch client for free
- Cold start and bundle size matter; the core has zero dependencies and the hono/tiny preset is under 12kB
- You depend on the Express/Fastify middleware universe (passport strategies, session stores, a decade of Stack Overflow answers); Hono's middleware set is much smaller and you will write more glue yourself
- You are building a large Node-only monolith and want a batteries-included framework with DI, ORM conventions and generators; Hono gives you routing and middleware, the rest is on you
- Your team writes plain JavaScript; Hono works but its main selling point is the TypeScript inference you would be leaving on the table
Setup reality
npm create hono@latest scaffolds per runtime and is genuinely quick. The friction is runtime differences: Node needs the extra @hono/node-server package, env vars are c.env on Workers but process.env on Node, and validation needs separate installs (@hono/zod-validator plus zod). Middleware that is one npm install in Express can mean reading the third-party middleware list or writing your own.
Patterns
Minimal app with routesbasic-app
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello'))
app.get('/health', (c) => c.json({ ok: true }))
export default appexport default is enough on Workers, Bun and Deno; Node needs @hono/node-server (next pattern).
Run a Hono app on Node.jsserve-on-node
import { serve } from '@hono/node-server'
import app from './app'
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`listening on :${info.port}`)
})@hono/node-server is a separate install; forgetting it is the most common first-run error on Node.
Read path params and query stringspath-params-query
app.get('/users/:id', (c) => {
const id = c.req.param('id')
const page = c.req.query('page') ?? '1'
return c.json({ id, page: Number(page) })
})c.req.query always returns strings or undefined; coerce and validate yourself or use a validator.
Validate a JSON body with zodvalidate-json-body
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const schema = z.object({ name: z.string(), age: z.number().int() })
app.post('/users', zValidator('json', schema), (c) => {
const body = c.req.valid('json') // fully typed
return c.json(body, 201)
})Both @hono/zod-validator and zod are separate installs; c.req.valid only works after the validator middleware ran.
Built-in logger plus custom timing middlewaremiddleware
import { logger } from 'hono/logger'
app.use(logger())
app.use(async (c, next) => {
const start = Date.now()
await next()
c.header('X-Response-Time', `${Date.now() - start}ms`)
})Middleware order matters: app.use registrations must come before the routes they should wrap.
Enable CORS for an API prefixcors
import { cors } from 'hono/cors'
app.use('/api/*', cors({
origin: 'https://app.example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))Scope it to a path pattern; a bare app.use(cors()) opens every route to any origin.
Protect routes with JWT middlewarejwt-auth
import { jwt } from 'hono/jwt'
app.use('/api/*', (c, next) =>
jwt({ secret: c.env?.JWT_SECRET ?? process.env.JWT_SECRET! })(c, next)
)
app.get('/api/me', (c) => c.json(c.get('jwtPayload')))On Workers secrets live on c.env, not process.env; wrapping the middleware in a handler lets one codebase serve both.
Central error handler with HTTPExceptionerror-handling
import { HTTPException } from 'hono/http-exception'
app.get('/admin', (c) => {
throw new HTTPException(403, { message: 'forbidden' })
})
app.onError((err, c) => {
if (err instanceof HTTPException) return err.getResponse()
console.error(err)
return c.json({ error: 'internal error' }, 500)
})Without app.onError uncaught errors become plain 500 text responses; register it once near the app root.
Typed fetch client with hono/clientrpc-typed-client
// server.ts
const routes = app.get('/hello', (c) => c.json({ msg: 'hi' }))
export type AppType = typeof routes
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
const res = await client.hello.$get()
const data = await res.json() // { msg: string }Type inference only flows if routes are chained (const routes = app.get(...).post(...)); registering routes as separate statements loses the types.
Stream server-sent eventsstreaming-sse
import { streamSSE } from 'hono/streaming'
app.get('/events', (c) =>
streamSSE(c, async (stream) => {
let i = 0
while (i < 10) {
await stream.writeSSE({ data: JSON.stringify({ tick: i++ }) })
await stream.sleep(1000)
}
})
)Behind nginx or a proxy, disable response buffering or events arrive in one late batch.
Split routes into sub-appsroute-groups
const users = new Hono()
users.get('/', (c) => c.json([]))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
app.route('/users', users)Sub-app middleware stays scoped to the mounted prefix, which keeps auth boundaries obvious.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| express | npm | Node-only app where you want the largest middleware ecosystem and every answer already written |
| fastify | npm | Node-only API where raw throughput, schema validation and a mature plugin system matter more than edge portability |
| elysia | npm | You are all-in on Bun and want a similar typed-DX framework tuned for that runtime |