Hono Best Practices
Quick Start
Installation & Version
Hono is a small, simple, and ultrafast web framework built on Web Standards. It works on any JavaScript runtime.
# npm
npm create hono@latest
# yarn
yarn create hono
# pnpm
pnpm create hono@latest
# bun
bun create hono@latest
# deno
deno init
deno add npm:honoMinimal Example
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default appCore Concepts
Ultrafast Routing
Hono uses RegExpRouter - no linear loops. Routes are matched using regular expressions, making it extremely fast.
import { Hono } from 'hono'
const app = new Hono()
// Basic route
app.get('/users', (c) => c.json({ users: [] }))
// Path parameters (fully typed)
app.get('/users/:id', (c) => {
const { id } = c.req.param()
return c.json({ user: { id } })
})
// Optional parameters
app.get('/posts/:slug?', (c) => {
const { slug } = c.req.param()
return c.json({ slug: slug ?? 'default' })
})
// Regex constraints
app.get('/items/:date{[0-9]+}', (c) => {
return c.text('Date pattern matched')
})Context Object
The Context object (c) is instantiated for each request and provides all necessary utilities:
app.get('/example', (c) => {
// Request access
const method = c.req.method
const path = c.req.path
const query = c.req.query('name')
const header = c.req.header('Authorization')
// Response helpers
return c.json({ message: 'OK' }, 200)
// Or: c.text(), c.html(), c.body()
// Set status
c.status(201)
// Set headers
c.header('X-Custom', 'value')
// Not found response
return c.notFound()
})Middleware Patterns
Middleware Execution Order
Middleware executes strictly in registration order. They wrap like onion layers:
middleware 1 start
middleware 2 start
handler
middleware 2 end
middleware 1 endimport { logger } from 'hono/logger'
app.use(logger()) // runs first for all routes
app.use(authenticate()) // runs second
app.get('/foo', (c) => c.text('foo'))Path-Specific Middleware
// Apply to specific path
app.use('/api/*', cors())
// Apply to specific method + path
app.post('/users/*', basicAuth())Custom Middleware with Type Safety
Use createMiddleware() for reusable, typed middleware:
import { createMiddleware } from 'hono/factory'
const logger = createMiddleware(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
})
// Typed variables
const authMiddleware = createMiddleware<{
Variables: { user: { id: string; name: string } }
}>(async (c, next) => {
const user = await verifyToken(c)
c.set('user', user)
await next()
})
// Usage with automatic type merging
app
.use(authMiddleware)
.get('/', (c) => {
const user = c.var.user // fully typed
return c.json({ name: user.name })
})Modifying Response After Handler
const addTimestamp = createMiddleware(async (c, next) => {
await next()
const originalText = await c.res.text()
c.res = new Response(`[${Date.now()}] ${originalText}`)
})Routing Architecture
Scaling with app.route()
For larger applications, create separate route files and mount them:
// routes/users.ts
import { Hono } from 'hono'
export const users = new Hono()
users.get('/', (c) => c.json({ users: [] }))
users.post('/', (c) => c.json({ id: '1' }, 201))
// routes/books.ts
import { Hono } from 'hono'
export const books = new Hono()
books.get('/', (c) => c.json({ books: [] }))
// index.ts
import { Hono } from 'hono'
import { users } from './routes/users'
import { books } from './routes/books'
const app = new Hono()
app.route('/api/users', users)
app.route('/api/books', books)
export default appBase Path
const api = new Hono().basePath('/api')
api.get('/posts', (c) => c.json({ posts: [] }))
app.route('/api', api)Type-Safe APIs with RPC
Server Setup
Export your app’s type for client inference:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('/api/*', cors())
app.get('/api/posts', (c) => {
return c.json([{ id: 1, title: 'Hello' }])
})
app.post('/api/posts', async (c) => {
const { title } = await c.req.json()
return c.json({ id: 2, title }, 201)
})
export type AppType = typeof app
export default appClient Usage
import { hc } from 'hono/client'
const client = hc<AppType>('http://localhost:8787/')
// Fully typed request/response
const res = await client.api.posts.$get()
const posts = await res.json()
// POST with typed body
const newPost = await client.api.posts.$post({
json: { title: 'New Post' }
})Status Code Typing
For explicit status codes in type inference:
// Good - status code is typed
app.get('/posts/:id', (c) => {
const post = findPost(c.req.param('id'))
if (!post) {
return c.json({ error: 'Not found' }, 404) // typed as 404
}
return c.json(post) // typed as 200
})
// Alternative using HTTPException
app.get('/posts/:id', (c) => {
const post = findPost(c.req.param('id'))
if (!post) {
throw new HTTPException(404, { message: 'Not found' })
}
return c.json(post)
})Validation with Zod
Using @hono/zod-validator
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1),
body: z.string().optional(),
})
const route = app.post(
'/posts',
zValidator('json', postSchema),
(c) => {
const { title, body } = c.req.valid('json')
return c.json({ id: crypto.randomUUID(), title, body }, 201)
}
)
export type RouteType = typeof routeUsing @hono/standard-validator
Supports Zod, Valibot, and ArkType with a unified API:
import { sValidator } from '@hono/standard-validator'
import { z } from 'zod'
const schema = z.object({
name: z.string(),
age: z.number(),
})
app.post('/author', sValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, message: `${data.name} is ${data.age}` })
})Error Handling
HTTPException
import { HTTPException } from 'hono/http-exception'
app.post('/users', async (c) => {
const body = await c.req.json()
if (!body.email) {
throw new HTTPException(400, { message: 'Email is required' })
}
const user = await createUser(body)
return c.json(user, 201)
})
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error(`${err}`)
return c.text('Internal Server Error', 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})Built-in Middleware
Hono includes comprehensive built-in middleware:
import {
cors,
logger,
basicAuth,
bearerAuth,
jwt,
compress,
poweredBy,
methodOverride,
} from 'hono/middleware'Common Middleware Usage
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
import { compress } from 'hono/compress'
const app = new Hono()
app.use(logger())
app.use(compress())
app.use(cors({
origin: ['https://example.com'],
credentials: true,
}))
// JWT Authentication
app.use('/api/*', jwt({
secret: process.env.JWT_SECRET,
}))
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: process.env.ADMIN_PASSWORD,
}))Multi-Runtime Deployment
Hono works seamlessly across different runtimes:
Cloudflare Workers
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('/*', cors())
app.get('/hello', (c) => {
return c.json({
message: 'Hello from Cloudflare Workers!',
env: c.env.ASSETS, // Cloudflare binding
})
})
export default {
fetch: app.fetch,
scheduled: app.scheduled,
}Deno
import { Hono } from 'https://deno.land/x/hono/mod.ts'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Deno!'))
Deno.serve(app.fetch)Bun
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Bun!'))
export default {
port: 3000,
fetch: app.fetch,
}Node.js (with Adapter)
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('/*', cors())
app.get('/', (c) => c.text('Hello from Node.js!'))
serve({
fetch: app.fetch,
port: 3000,
})Performance Tips
Use Presets Appropriately
Hono offers different presets balancing size and features:
// hono/tiny - smallest, ~14KB
import { Hono } from 'hono/tiny'
// hono/base - includes more features
import { Hono } from 'hono/base'
// hono - full-featured (default)
import { Hono } from 'hono'Avoid Controller-like Patterns
// Good - proper type inference
app.get('/users/:id', (c) => {
const { id } = c.req.param()
return c.json({ id })
})
// Avoid - loses type inference for path parameters
const getUser = (c) => c.json({ id: c.req.param('id') })
app.get('/users/:id', controller(getUser))Use Factory Pattern When Needed
If you prefer organized controllers, use factory.createHandlers():
import { createFactory } from 'hono/factory'
const factory = createFactory()
const handlers = factory.createHandlers(
logger(),
authenticate(),
(c) => {
return c.json(c.var.user)
}
)
app.get('/profile', ...handlers)Testing
Using app.request()
describe('API', () => {
it('GET /posts returns posts list', async () => {
const res = await app.request('/posts')
expect(res.status).toBe(200)
const body = await res.json()
expect(body.posts).toBeDefined()
})
it('POST /posts creates new post', async () => {
const res = await app.request('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test' }),
})
expect(res.status).toBe(201)
})
it('Mock environment variables', async () => {
const res = await app.request('/kv/test', {}, {
Bindings: { MY_KV: { get: () => 'value' } },
})
expect(res.status).toBe(200)
})
})Project Structure Recommendation
src/
├── index.ts # Main entry
├── routes/
│ ├── users.ts # User routes
│ ├── posts.ts # Post routes
│ └── api/
│ └── v1.ts # API v1
├── middleware/
│ ├── auth.ts
│ └── logger.ts
├── lib/
│ └── db.ts
└── types/
└── index.ts # Shared typesSummary
Hono provides a lightweight yet powerful web framework with:
- Ultrafast routing via RegExpRouter
- Full TypeScript support with RPC type inference
- Middleware composability with predictable execution order
- Multi-runtime support (Cloudflare, Deno, Bun, Node.js, Lambda)
- Built-in middleware for common tasks (CORS, Auth, Logging)
- Zod validation integration for type-safe input
The key to effective Hono development: leverage TypeScript types, compose middleware wisely, and use app.route() for scalable architecture.