mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmWeb Backendupdated 05 Aug 2026

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.

Verdict

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.

API stability4/5v4 has been the stable line since early 2024 with frequent non-breaking minors; churn shows up mostly in helper packages and middleware, not the core routing API.
Docs4/5hono.dev covers every runtime with concrete examples and a migration guide; some third-party middleware pages are thin and you end up reading source.
Maintenance5/5Very active: pushed within the last day, frequent releases, responsive maintainer team led by Yusuke Wada, 31k+ stars and a busy Discord.
Ecosystem3/5Built-in and official middleware cover auth, CORS, JWT and validation, but the third-party pool is a fraction of Express's; niche needs mean writing your own middleware.

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
Skip it if

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 app

export 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

PackageRegistryPick it when
expressnpmNode-only app where you want the largest middleware ecosystem and every answer already written
fastifynpmNode-only API where raw throughput, schema validation and a mature plugin system matter more than edge portability
elysianpmYou are all-in on Bun and want a similar typed-DX framework tuned for that runtime