Hono Patterns
Core patterns for building Hono applications.
Routing Patterns
Modular Route Organization
Organize routes in separate files and compose with app.route():
// routes/users.ts
import { Hono } from 'hono'
import type { Env } from '../types/bindings'
const users = new Hono<Env>()
users.get('/', (c) => c.json({ users: [] }))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', (c) => c.json({ created: true }, 201))
export { users }
// index.ts
import { users } from './routes/users'
import { posts } from './routes/posts'
app.route('/api/users', users)
app.route('/api/posts', posts)
Route Groups with Shared Middleware
const api = new Hono<Env>()
// Apply auth to all /api routes
api.use('*', authMiddleware)
api.route('/users', users)
api.route('/posts', posts)
app.route('/api', api)
// Public routes remain unprotected
app.get('/health', (c) => c.json({ status: 'ok' }))
Chained Route Definition (for RPC)
// Chain routes for proper type inference
const routes = app
.get('/users', (c) => c.json({ users: [] }))
.post('/users', (c) => c.json({ created: true }, 201))
.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
export type AppType = typeof routes
Handler Patterns
Basic Handler
app.get('/users', async (c) => {
const users = await fetchUsers()
return c.json(users)
})
Handler with Validation
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
app.post('/users',
zValidator('json', z.object({
email: z.string().email(),
name: z.string().min(1)
})),
async (c) => {
const data = c.req.valid('json')
return c.json({ id: crypto.randomUUID(), ...data }, 201)
}
)
Factory Pattern for External Handlers
Use when handlers need to be defined outside route files:
import { createFactory } from 'hono/factory'
import type { Env } from '../types/bindings'
const factory = createFactory<Env>()
// Define handler with proper types
export const listUsers = factory.createHandlers(
zValidator('query', paginationSchema),
async (c) => {
const { page, limit } = c.req.valid('query')
return c.json({ users: [], page, limit })
}
)
// Use in routes
users.get('/', ...listUsers)
Middleware Patterns
Creating Typed Middleware
import { createMiddleware } from 'hono/factory'
import type { Env } from '../types/bindings'
export const loggerMiddleware = createMiddleware<Env>(async (c, next) => {
const start = Date.now()
await next()
console.log(`${c.req.method} ${c.req.url} - ${Date.now() - start}ms`)
})
Middleware with Variables
// Update types/bindings.ts
type Env = {
Variables: {
user: { id: string; email: string }
}
}
// Middleware sets variable
const authMiddleware = createMiddleware<Env>(async (c, next) => {
const user = await validateToken(c.req.header('Authorization'))
c.set('user', user) // Type-safe!
await next()
})
// Handler accesses variable
app.get('/profile', authMiddleware, (c) => {
const user = c.get('user') // Type-safe!
return c.json(user)
})
Configurable Middleware Factory
interface CacheOptions {
maxAge: number
public?: boolean
}
export const cache = (options: CacheOptions) => {
return createMiddleware<Env>(async (c, next) => {
await next()
const directive = options.public ? 'public' : 'private'
c.header('Cache-Control', `${directive}, max-age=${options.maxAge}`)
})
}
// Usage
app.get('/static/*', cache({ maxAge: 86400, public: true }))
Error Handling
HTTPException
import { HTTPException } from 'hono/http-exception'
app.get('/users/:id', async (c) => {
const user = await getUser(c.req.param('id'))
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json(user)
})
Global Error Handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: 'Internal server error' }, 500)
})
Custom Error Classes
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` })
}
}
class ValidationError extends HTTPException {
constructor(errors: Record<string, string>) {
super(400, { message: 'Validation failed', cause: errors })
}
}
Response Patterns
JSON Responses
// Success
c.json({ data: users })
c.json({ data: user }, 200)
c.json({ data: newUser }, 201)
// Errors
c.json({ error: 'Not found' }, 404)
c.json({ error: 'Validation failed', details: errors }, 400)
// No content
c.body(null, 204)
Consistent Response Format
interface ApiResponse<T> {
data?: T
error?: string
meta?: {
page?: number
limit?: number
total?: number
}
}
// Helper
function success<T>(c: Context, data: T, status = 200) {
return c.json({ data } as ApiResponse<T>, status)
}
function error(c: Context, message: string, status = 400) {
return c.json({ error: message } as ApiResponse<never>, status)
}
Context Access
Request Data
// Path params
const id = c.req.param('id')
const { id, slug } = c.req.param()
// Query params
const page = c.req.query('page')
const { page, limit } = c.req.query()
// Headers
const auth = c.req.header('Authorization')
// Body (validated)
const data = c.req.valid('json')
const form = c.req.valid('form')
const query = c.req.valid('query')
Environment and Bindings
// Environment variables
const secret = c.env.JWT_SECRET
// Cloudflare bindings
const db = c.env.DB
const kv = c.env.KV
const bucket = c.env.BUCKET
Request Testing via CLI and app.request
Test endpoints directly without launching a background server:
// Fast in-memory request testing (Bun test / Vitest)
import { describe, it, expect } from 'bun:test'
import app from '../src/index'
describe('User API', () => {
it('GET /api/users returns 200', async () => {
const res = await app.request('/api/users')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ users: [] })
})
it('POST /api/users validates payload', async () => {
const res = await app.request('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@example.com' }),
})
expect(res.status).toBe(201)
})
})
Using the Hono CLI:
# Test endpoints directly from terminal
npx hono request http://localhost:8787/api/users
OpenAPI & Swagger via @hono/zod-openapi
import { createRoute, OpenAPIHono, 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() }),
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'Retrieve user by ID',
},
},
})
app.openapi(getUserRoute, (c) => {
const { id } = c.req.valid('param')
return c.json({ id, name: 'John Doe' }, 200)
})
// Auto-generated Swagger UI docs
app.doc('/doc', { openapi: '3.1.0', info: { title: 'API', version: 'v1' } })
app.get('/ui', swaggerUI({ url: '/doc' }))
AI Streaming & Server-Sent Events (SSE)
import { streamText, streamSSE } from 'hono/streaming'
app.get('/api/chat', (c) => {
return streamText(c, async (stream) => {
for (const chunk of ['Hello', ' from', ' edge!']) {
await stream.write(chunk)
await stream.sleep(100)
}
})
})
Best Practices
- Always type your app:
new Hono<Env>() - Validate all inputs with Zod
- Use
createMiddlewarefor type-safe middleware - Export
AppTypefor RPC client - Handle errors with HTTPException
- Use proper status codes: 200, 201, 204, 400, 401, 404, 500
- Organize routes in separate files
- Keep handlers small - delegate to services