TL;DR
Hono is an ultra-fast, multi-runtime web framework (2.5x faster than Express) with built-in TypeScript support, middleware, validation via Zod, JWT auth, CORS, OpenAPI docs, and RPC mode for end-to-end type safety. It runs everywhere: Cloudflare Workers, Deno, Bun, Node.js, and AWS Lambda.
Key Takeaways
- Runs on every major JS runtime with zero code changes â true write-once, deploy-anywhere.
- Built-in middleware for JWT, CORS, ETag, logging, and compression eliminates external packages.
- Zod-based validation provides runtime type checking with automatic TypeScript inference.
- RPC mode enables end-to-end type safety between server and client without code generation.
- OpenAPI integration auto-generates Swagger docs from your route definitions.
- Handles 130K+ req/s on Bun, outperforming Express by 2.5x or more.
Hono is a small, simple, and ultra-fast web framework designed for the edge. With first-class TypeScript support and zero dependencies, Hono runs on virtually every JavaScript runtime: Cloudflare Workers, Deno, Bun, Node.js, Fastly, Netlify, AWS Lambda, and more. It delivers Express-like ergonomics with dramatically better performance and full type safety.
What Is Hono?
Hono (meaning "flame" in Japanese) is a lightweight web framework created by Yusuke Wada in 2022. Originally designed for Cloudflare Workers, it now supports every major JavaScript runtime. Hono uses Web Standard APIs (Request, Response, fetch) as its foundation, making it portable across runtimes without polyfills.
- Ultra-fast: RegExpRouter benchmarks at 130K+ req/s on Bun
- Tiny: core ~14KB minified, zero external dependencies
- Multi-runtime: Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, Fastly
- Type-safe: first-class TypeScript with inferred route types and RPC mode
- Rich middleware: JWT, CORS, ETag, logger, compress, basicAuth, secureHeaders built-in
- Web Standards: built on Request/Response, no proprietary abstractions
Installation and Setup
Hono provides starter templates for every runtime. Use the create-hono CLI to scaffold a new project.
# Create a new Hono project
npm create hono@latest my-app
# Select your target runtime:
# cloudflare-workers / deno / bun / nodejs / aws-lambda
cd my-app && npm install
# Or install manually
npm install hono
# Project structure
# my-app/
# âââ src/index.ts # Entry point
# âââ package.json
# âââ tsconfig.json
# âââ wrangler.toml # (Cloudflare only)Routing
Hono provides a powerful routing system with path parameters, wildcards, regex constraints, and optional segments.
Basic Routes
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ id: 1, ...body }, 201)
})
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
return c.json({ id, ...body })
})
app.delete('/users/:id', (c) => {
return c.json({ deleted: c.req.param('id') })
})
app.on(['PUT', 'PATCH'], '/items/:id', (c) => {
return c.json({ updated: true })
})
app.all('/api/*', (c) => c.json({ method: c.req.method }))
export default appGrouped and Nested Routes
Use app.route() to organize routes into logical groups, keeping large applications maintainable.
import { Hono } from 'hono'
// routes/users.ts
const users = new Hono()
users.get('/', (c) => c.json([{ id: 1, name: 'Alice' }]))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', async (c) => c.json(await c.req.json(), 201))
// routes/posts.ts
const posts = new Hono()
posts.get('/', (c) => c.json([{ id: 1, title: 'Hello' }]))
posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))
// Main app â mount route groups
const app = new Hono()
app.route('/api/users', users)
app.route('/api/posts', posts)
export default appRoute Parameters and Wildcards
// Named parameters
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
// Multiple parameters
app.get('/orgs/:orgId/repos/:repoId', (c) => {
const { orgId, repoId } = c.req.param()
return c.json({ orgId, repoId })
})
// Optional parameter
app.get('/articles/:slug/:format?', (c) => {
const format = c.req.param('format') || 'html'
return c.json({ slug: c.req.param('slug'), format })
})
// Wildcard
app.get('/files/*', (c) => c.text('File: ' + c.req.path))
// Regex constraint
app.get('/posts/:id{[0-9]+}', (c) => {
return c.json({ id: Number(c.req.param('id')) })
})Middleware
Middleware in Hono follows the onion model. Hono ships with rich built-in middleware and makes custom ones simple to create.
Built-in Middleware
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { etag } from 'hono/etag'
import { compress } from 'hono/compress'
import { secureHeaders } from 'hono/secure-headers'
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
app.use('*', logger()) // Log method, path, status
app.use('/api/*', cors()) // Cross-origin requests
app.use('*', etag()) // Caching headers
app.use('*', compress()) // Gzip/brotli
app.use('*', secureHeaders()) // CSP, X-Frame-Options
app.use('*', prettyJSON()) // ?pretty for formatted output
app.use('/admin/*', basicAuth({
username: 'admin', password: 'secret',
}))
app.use('/api/private/*', bearerAuth({
token: 'my-secret-token',
}))Custom Middleware
import { createMiddleware } from 'hono/factory'
// Timing middleware
const timer = createMiddleware(async (c, next) => {
const start = Date.now()
await next()
c.header('X-Response-Time', (Date.now() - start) + 'ms')
})
// Typed auth middleware
type Env = { Variables: { user: { id: string; role: string } } }
const auth = createMiddleware<Env>(async (c, next) => {
const token = c.req.header('Authorization')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
c.set('user', { id: '123', role: 'admin' })
await next()
})
const app = new Hono<Env>()
app.use('*', timer)
app.use('/protected/*', auth)
app.get('/protected/profile', (c) => {
const user = c.get('user') // fully typed!
return c.json({ user })
})Request Handling
The Hono context object (c) provides convenient methods to access headers, query params, body, and more.
app.post('/upload', async (c) => {
// Headers
const contentType = c.req.header('Content-Type')
// Query parameters
const page = c.req.query('page') // single
const tags = c.req.queries('tag') // array
// JSON body
const json = await c.req.json()
// Form data
const form = await c.req.formData()
const file = form.get('file') as File
// URL info
const url = c.req.url // full URL
const path = c.req.path // pathname
const method = c.req.method // HTTP method
// Path parameters
const id = c.req.param('id')
// Raw body
const text = await c.req.text()
const buffer = await c.req.arrayBuffer()
return c.json({ received: true })
})Response Helpers
Hono provides ergonomic response helpers for JSON, HTML, text, redirects, streaming, and more.
// JSON, text, HTML responses
app.get('/json', (c) => c.json({ message: 'hello' }))
app.get('/error', (c) => c.json({ error: 'not found' }, 404))
app.get('/text', (c) => c.text('Hello World'))
app.get('/page', (c) => c.html('<h1>Hello</h1>'))
// Redirects
app.get('/old', (c) => c.redirect('/new'))
app.get('/moved', (c) => c.redirect('/permanent', 301))
// Headers and status
app.get('/custom', (c) => {
c.header('X-Custom', 'value')
c.header('Cache-Control', 'max-age=3600')
c.status(201)
return c.json({ created: true })
})
// Streaming
app.get('/stream', (c) => {
return c.streamText(async (stream) => {
for (let i = 0; i < 5; i++) {
await stream.writeln('Chunk ' + i)
await stream.sleep(1000)
}
})
})Validation with Zod
Hono integrates with Zod through @hono/zod-validator, providing runtime validation with automatic TypeScript type inference.
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
// Validate JSON body
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).optional(),
})
app.post('/users', zValidator('json', userSchema), (c) => {
const body = c.req.valid('json') // fully typed
return c.json({ user: body }, 201)
})
// Validate query parameters
const listSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().max(100).default(20),
search: z.string().optional(),
})
app.get('/users', zValidator('query', listSchema), (c) => {
const { page, limit, search } = c.req.valid('query')
return c.json({ page, limit, search })
})
// Validate path params + custom error handling
app.get('/users/:id',
zValidator('param', z.object({ id: z.coerce.number().positive() }),
(result, c) => {
if (!result.success) {
return c.json({ errors: result.error.flatten() }, 422)
}
}
),
(c) => c.json({ id: c.req.valid('param').id })
)The Context Object
The context object (c) is the core of every Hono handler â it provides access to request, response helpers, variables, and execution context.
type AppEnv = {
Bindings: { DATABASE_URL: string; API_KEY: string }
Variables: { requestId: string }
}
const app = new Hono<AppEnv>()
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
app.get('/demo', (c) => {
const url = c.req.url // request URL
const dbUrl = c.env.DATABASE_URL // env bindings
const reqId = c.get('requestId') // typed variables
c.header('X-Request-Id', reqId) // set headers
return c.json({ requestId: reqId, url })
})
// waitUntil (Cloudflare Workers)
app.post('/webhook', (c) => {
c.executionCtx.waitUntil(
fetch('https://analytics.example.com/track', {
method: 'POST',
body: JSON.stringify({ event: 'webhook' }),
})
)
return c.json({ ok: true })
})Runtime Adapters
One of Hono's greatest strengths is multi-runtime support. The same code runs across different runtimes with minimal adapter changes.
Cloudflare Workers
// src/index.ts â Cloudflare Workers
import { Hono } from 'hono'
type Bindings = {
MY_KV: KVNamespace
MY_DB: D1Database
MY_BUCKET: R2Bucket
API_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/kv/:key', async (c) => {
const val = await c.env.MY_KV.get(c.req.param('key'))
return val ? c.json({ val }) : c.json({ error: 'Not found' }, 404)
})
app.get('/db/users', async (c) => {
const { results } = await c.env.MY_DB
.prepare('SELECT * FROM users LIMIT 10')
.all()
return c.json(results)
})
export default appDeno
// main.ts â Deno
import { Hono } from 'https://deno.land/x/hono/mod.ts'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Deno!'))
app.get('/env', (c) => {
return c.json({ runtime: 'deno', port: Deno.env.get('PORT') })
})
Deno.serve(app.fetch)
// Run: deno run --allow-net --allow-env main.tsBun
// index.ts â Bun (fastest runtime for Hono)
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Bun!'))
app.get('/file', async (c) => {
const data = await Bun.file('./data.json').json()
return c.json(data)
})
export default { port: 3000, fetch: app.fetch }
// Run: bun run index.tsNode.js
// index.ts â Node.js
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Node.js!'))
app.get('/health', (c) => {
return c.json({ status: 'ok', version: process.version })
})
serve({ fetch: app.fetch, port: 3000 })
// Install: npm install hono @hono/node-server
// Run: npx tsx index.tsAWS Lambda
// lambda.ts â AWS Lambda
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Lambda!'))
app.get('/items', (c) => {
return c.json([{ id: 1, name: 'Item A' }])
})
// Export handler for API Gateway or Lambda URL
export const handler = handle(app)Error Handling
Hono provides structured error handling through app.onError() and the HTTPException class for clean, consistent API error responses.
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json(
{ error: err.message, status: err.status },
err.status
)
}
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found', path: c.req.path }, 404)
})
// Throw HTTPException in handlers
app.get('/users/:id', async (c) => {
const user = await findUser(c.req.param('id'))
if (!user) {
throw new HTTPException(404, {
message: 'User not found',
})
}
return c.json(user)
})
// Custom error with response
app.get('/protected', (c) => {
throw new HTTPException(401, {
message: 'Authentication required',
res: new Response('Unauthorized', {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer' },
}),
})
})Testing
Hono provides a built-in test client via app.request() â no need to spin up a server.
// app.test.ts â using Vitest
import { describe, it, expect } from 'vitest'
import app from './app'
describe('API', () => {
it('GET / returns OK', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(await res.text()).toBe('OK')
})
it('GET /users/:id returns user', async () => {
const res = await app.request('/users/42')
const data = await res.json()
expect(data.id).toBe('42')
})
it('POST /users creates user', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data.name).toBe('Alice')
})
it('returns 404 for unknown routes', async () => {
const res = await app.request('/nonexistent')
expect(res.status).toBe(404)
})
it('handles auth with Bearer token', async () => {
const res = await app.request('/api/me', {
headers: { Authorization: 'Bearer my-token' },
})
expect(res.status).toBe(200)
})
})JWT Authentication
Hono includes built-in JWT middleware for token verification, enabling complete authentication flows.
import { Hono } from 'hono'
import { jwt, sign } from 'hono/jwt'
const app = new Hono()
const SECRET = 'my-jwt-secret-key'
// Login â generate token
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
if (email !== 'admin@test.com') {
return c.json({ error: 'Invalid credentials' }, 401)
}
const token = await sign({
sub: 'user-123', email, role: 'admin',
exp: Math.floor(Date.now() / 1000) + 3600,
}, SECRET)
return c.json({ token })
})
// Protect routes
app.use('/api/*', jwt({ secret: SECRET }))
app.get('/api/me', (c) => {
const payload = c.get('jwtPayload')
return c.json({ userId: payload.sub, role: payload.role })
})
// Role-based access control
const requireRole = (role: string) => async (c: any, next: any) => {
if (c.get('jwtPayload').role !== role) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
app.delete('/api/admin/users/:id', requireRole('admin'),
(c) => c.json({ deleted: c.req.param('id') })
)CORS Configuration
The built-in CORS middleware handles cross-origin requests with fine-grained control over origins, methods, and headers.
import { cors } from 'hono/cors'
// Allow all origins (development)
app.use('/api/*', cors())
// Restrict to specific origins (production)
app.use('/api/*', cors({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['X-Total-Count'],
maxAge: 86400,
credentials: true,
}))
// Dynamic origin
app.use('/api/*', cors({
origin: (origin) => {
return origin.endsWith('.myapp.com') ? origin : 'https://myapp.com'
},
}))OpenAPI Integration
The @hono/zod-openapi package defines routes with OpenAPI schemas and auto-generates Swagger documentation.
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
const app = new OpenAPIHono()
const getUserRoute = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().openapi({ example: '123' }),
}),
},
responses: {
200: {
content: { 'application/json': {
schema: z.object({
id: z.string(), name: z.string(), email: z.string().email(),
}),
}},
description: 'User found',
},
},
})
app.openapi(getUserRoute, (c) => {
const { id } = c.req.valid('param')
return c.json({ id, name: 'Alice', email: 'a@b.com' })
})
// Serve OpenAPI spec + Swagger UI
app.doc('/doc', {
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
})
app.get('/ui', swaggerUI({ url: '/doc' }))RPC Mode
Hono RPC provides end-to-end type safety between server and client without code generation. The client infers types directly from route definitions.
// ---- Server (server.ts) ----
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/api/posts', (c) => {
return c.json([{ id: 1, title: 'Hello Hono' }])
})
.post('/api/posts',
zValidator('json', z.object({
title: z.string(), content: z.string(),
})),
(c) => c.json({ id: 2, ...c.req.valid('json') }, 201)
)
export type AppType = typeof app
export default app
// ---- Client (client.ts) ----
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
// Fully typed â IDE autocomplete + compile-time checks
const res = await client.api.posts.$get()
const data = await res.json() // typed as { id: number; title: string }[]
const newPost = await client.api.posts.$post({
json: { title: 'New', content: 'Hello!' },
}) // TypeScript error if shape is wrongPerformance Benchmarks
Hono consistently outperforms Express, Fastify, and Koa in benchmarks.
Benchmark Results (simple JSON, 10s)
- Hono (Bun): ~130,000 req/s
- Fastify (Node.js): ~75,000 req/s
- Express (Node.js): ~50,000 req/s
- Hono (Deno): ~95,000 req/s
- Hono (Workers): sub-ms cold start, ~80,000 req/s
// Performance tips
// 1. RegExpRouter (default) â fastest for static routes
const app = new Hono() // uses RegExpRouter
// 2. LinearRouter â better for dynamic route registration
import { LinearRouter } from 'hono/router/linear-router'
const app = new Hono({ router: new LinearRouter() })
// 3. Use c.json() over new Response()
app.get('/fast', (c) => c.json({ ok: true }))
// 4. Stream large responses
app.get('/large', (c) => {
return c.streamText(async (stream) => {
for (const chunk of data) await stream.write(chunk)
})
})
// 5. Cache with ETag
import { etag } from 'hono/etag'
app.use('/api/*', etag())Frequently Asked Questions
What is Hono and how is it different from Express?
Hono is an ultra-fast, lightweight web framework built on Web Standard APIs. Unlike Express which only runs on Node.js, Hono runs on Cloudflare Workers, Deno, Bun, Node.js, and AWS Lambda. It is 2-3x faster, has first-class TypeScript support, zero dependencies, and built-in middleware for JWT, CORS, and validation.
Can Hono replace Express in production?
Yes. Hono is production-ready and used by Cloudflare, Vercel, and many startups. It provides all Express features plus type safety, multi-runtime support, and significantly better performance.
Which JavaScript runtime is fastest for Hono?
Bun delivers the highest throughput at 130K+ req/s. Deno is second at ~95K req/s. Cloudflare Workers provide the lowest latency due to edge deployment. On Node.js, Hono still outperforms Express by 2x+.
How does Hono handle validation?
Hono integrates with Zod through @hono/zod-validator. You define schemas for body, query, headers, and params. The middleware validates automatically, returning 400 errors for invalid data with full TypeScript inference.
Does Hono support WebSockets?
Yes. Hono supports WebSocket connections through hono/websocket on runtimes that support it (Cloudflare Workers with Durable Objects, Deno, Bun) with full type safety.
How do I deploy Hono to Cloudflare Workers?
Create a project with "npm create hono@latest" and select cloudflare-workers. Write routes in src/index.ts, configure wrangler.toml, then deploy with "wrangler deploy". Hono has first-class Cloudflare support with typed bindings.
What is Hono RPC mode?
RPC mode provides end-to-end type safety between server and client without code generation. Export the app type from server, use hc() to create a typed client that infers all route types and response shapes at compile time.
Can Hono generate OpenAPI documentation automatically?
Yes. The @hono/zod-openapi package auto-generates a complete OpenAPI 3.0 spec from route definitions. You can serve Swagger UI directly from your Hono app, eliminating separate documentation maintenance.