Last Updated: 3/9/2026
Best Practices
Hono is flexible and unopinionated, but following these patterns will help you build maintainable, type-safe applications.
Avoid “Controllers” When Possible
Don’t create Ruby on Rails-style controllers that separate route definitions from handlers.
The Problem with Controllers
Separating handlers loses type inference:
// ❌ Don't do this
const booksList = (c: Context) => {
return c.json('list books')
}
app.get('/books', booksList)Path parameters can’t be inferred:
// ❌ Type inference doesn't work
const bookPermalink = (c: Context) => {
const id = c.req.param('id') // Can't infer the path param type
return c.json(`get ${id}`)
}
app.get('/books/:id', bookPermalink)The Solution: Inline Handlers
Define handlers inline to preserve type inference:
// ✅ Do this instead
app.get('/books/:id', (c) => {
const id = c.req.param('id') // Type is correctly inferred as string
return c.json(`get ${id}`)
})Using factory.createHandlers() for Reusable Logic
If you need to extract handler logic, use factory.createHandlers() from hono/factory to maintain type safety:
import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'
const factory = createFactory()
// Create middleware with proper types
const middleware = factory.createMiddleware(async (c, next) => {
c.set('foo', 'bar')
await next()
})
// Create handlers that preserve types
const handlers = factory.createHandlers(logger(), middleware, (c) => {
return c.json(c.var.foo) // Type-safe access to 'foo'
})
app.get('/api', ...handlers)This approach maintains type inference while allowing code reuse.
Building Larger Applications
For larger applications, organize routes into separate files and use app.route() to compose them.
File Structure
Organize by feature or resource:
src/
├── index.ts # Main app
├── authors.ts # Authors routes
└── books.ts # Books routesRoute Modules
Create separate Hono instances for each resource:
authors.ts:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.json('list authors'))
app.post('/', (c) => c.json('create an author', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default appbooks.ts:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default appComposing Routes
Mount route modules on the main app:
index.ts:
import { Hono } from 'hono'
import authors from './authors'
import books from './books'
const app = new Hono()
app.route('/authors', authors)
app.route('/books', books)
export default appThis creates these endpoints:
GET /authors→ list authorsPOST /authors→ create authorGET /authors/:id→ get authorGET /books→ list booksPOST /books→ create bookGET /books/:id→ get book
Enabling RPC Type Inference
When using RPC features, chain route definitions to preserve types:
authors.ts:
import { Hono } from 'hono'
// Chain methods to preserve types
const app = new Hono()
.get('/', (c) => c.json('list authors'))
.post('/', (c) => c.json('create an author', 201))
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default app
export type AppType = typeof app // Export the typeclient.ts:
import type { AppType } from './authors'
import { hc } from 'hono/client'
// Fully typed client
const client = hc<AppType>('http://localhost:8787')
const res = await client.index.$get() // Type-safe!For complete RPC setup in larger apps, see Using RPC with Larger Applications.
Environment Variables and Bindings
Define types for environment variables and platform bindings:
type Bindings = {
DATABASE_URL: string
API_KEY: string
MY_KV: KVNamespace
}
type Variables = {
user: User
}
const app = new Hono<{
Bindings: Bindings
Variables: Variables
}>()
app.use('/api/*', async (c, next) => {
// Typed access to env
const apiKey = c.env.API_KEY
const data = await c.env.MY_KV.get('key')
await next()
})Middleware Organization
Apply middleware in the correct order:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// 1. Global middleware (runs for all routes)
app.use('*', logger())
app.use('*', secureHeaders())
// 2. Route-specific middleware
app.use('/api/*', cors())
// 3. Route handlers
app.get('/api/posts', (c) => c.json({ posts: [] }))Middleware executes in registration order, so place global middleware first.
Error Handling Patterns
Set up centralized error handling:
import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
if (err instanceof HTTPException) {
// Handle HTTP exceptions
return err.getResponse()
}
// Log unexpected errors
console.error(err)
return c.json(
{ error: 'Internal Server Error' },
500
)
})Throw HTTP exceptions in handlers:
import { HTTPException } from 'hono/http-exception'
app.get('/posts/:id', async (c) => {
const id = c.req.param('id')
const post = await getPost(id)
if (!post) {
throw new HTTPException(404, { message: 'Post not found' })
}
return c.json({ post })
})Validation Best Practices
Always validate input at route boundaries:
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1).max(100),
body: z.string().min(1),
published: z.boolean().default(false),
})
app.post(
'/posts',
zValidator('json', postSchema),
async (c) => {
const data = c.req.valid('json') // Fully typed and validated
// ... create post
return c.json({ message: 'Created' }, 201)
}
)See Validation for more details.
Testing Strategy
Write tests for your routes using app.request():
import { describe, test, expect } from 'vitest'
describe('Books API', () => {
test('GET /books returns list', async () => {
const res = await app.request('/books')
expect(res.status).toBe(200)
const data = await res.json()
expect(Array.isArray(data)).toBe(true)
})
test('POST /books creates book', async () => {
const res = await app.request('/books', {
method: 'POST',
body: JSON.stringify({ title: 'Test Book' }),
headers: { 'Content-Type': 'application/json' },
})
expect(res.status).toBe(201)
})
})See Testing Applications for comprehensive testing patterns.
What’s Next
- Testing Applications - Learn how to test your Hono apps
- RPC Client - Build type-safe API clients
- Deployment Overview - Deploy your application to production