| name | hono-routing |
| description | Build type-safe APIs with Hono - fast, lightweight routing for Cloudflare Workers, Deno, Bun, and Node.js. Set up routing patterns, middleware composition, request validation (Zod/Valibot/Typia/ArkType), RPC client/server with full type inference, and error handling with HTTPException.
Use when: building APIs with Hono, setting up request validation with schema libraries, creating type-safe RPC client/server communication, implementing custom middleware chains, handling errors with HTTPException, extending context with custom variables, or troubleshooting middleware type inference issues, validation hook confusion, RPC performance problems, or middleware response typing errors.
|
| license | MIT |
Hono Routing & Middleware
Status: Production Ready ✅
Last Updated: 2025-10-22
Dependencies: None (framework-agnostic)
Latest Versions: hono@4.10.2, zod@4.1.12, valibot@1.1.0, @hono/zod-validator@0.7.4, @hono/valibot-validator@0.5.3
Quick Start (15 Minutes)
1. Install Hono
npm install hono@4.10.2
Why Hono:
- Fast: Built on Web Standards, runs on any JavaScript runtime
- Lightweight: ~10KB, no dependencies
- Type-safe: Full TypeScript support with type inference
- Flexible: Works on Cloudflare Workers, Deno, Bun, Node.js, Vercel
2. Create Basic App
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.json({ message: 'Hello Hono!' })
})
export default app
CRITICAL:
- Use
c.json(), c.text(), c.html() for responses
- Return the response (don't use
res.send() like Express)
- Export app for runtime (Cloudflare Workers, Deno, Bun, Node.js)
3. Add Request Validation
npm install zod@4.1.12 @hono/zod-validator@0.7.4
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const schema = z.object({
name: z.string(),
age: z.number(),
})
app.post('/user', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
Why Validation:
- Type-safe request data
- Automatic error responses
- Runtime validation, not just TypeScript
The 6-Part Hono Mastery Guide
Part 1: Routing Patterns
Basic Routes
import { Hono } from 'hono'
const app = new Hono()
app.get('/posts', (c) => c.json({ posts: [] }))
app.post('/posts', (c) => c.json({ created: true }))
app.put('/posts/:id', (c) => c.json({ updated: true }))
app.delete('/posts/:id', (c) => c.json({ deleted: true }))
app.on(['GET', 'POST'], '/multi', (c) => c.text('GET or POST'))
app.all('/catch-all', (c) => c.text('Any method'))
Key Points:
- Always return a Response (c.json, c.text, c.html, etc.)
- Routes are matched in order (first match wins)
- Use specific routes before wildcard routes
Route Parameters
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
app.get('/files/*', (c) => {
const path = c.req.param('*')
return c.json({ filePath: path })
})
CRITICAL:
c.req.param('name') returns single parameter
c.req.param() returns all parameters as object
- Parameters are always strings (cast to number if needed)
Query Parameters
app.get('/search', (c) => {
const q = c.req.query('q')
const { page, limit } = c.req.query()
const tags = c.req.queries('tag')
return c.json({ q, page, limit, tags })
})
Best Practice:
- Use validation for query params (see Part 4)
- Provide defaults for optional params
- Parse numbers/booleans from query strings
Wildcard Routes
app.get('/api/*', (c) => {
const path = c.req.param('*')
return c.json({ catchAll: path })
})
app.get('/files/:filepath{.+}', (c) => {
const filepath = c.req.param('filepath')
return c.json({ file: filepath })
})
Route Grouping (Sub-apps)
const api = new Hono()
api.get('/users', (c) => c.json({ users: [] }))
api.get('/posts', (c) => c.json({ posts: [] }))
const app = new Hono()
app.route('/api', api)
Why Group Routes:
- Organize large applications
- Share middleware for specific routes
- Better code structure and maintainability
Part 2: Middleware Composition
Middleware Flow
import { Hono } from 'hono'
const app = new Hono()
app.use('*', async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
console.log('Response sent')
})
app.use('/admin/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
})
app.get('/admin/dashboard', (c) => {
return c.json({ message: 'Admin Dashboard' })
})
CRITICAL:
- Always call
await next() in middleware
- Middleware runs BEFORE the handler
- Return early to prevent handler execution
- Check
c.error AFTER next() for error handling
Built-in Middleware
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { compress } from 'hono/compress'
import { cache } from 'hono/cache'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors({
origin: 'https://example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
}))
app.use('*', prettyJSON())
app.use('*', compress())
app.use(
'/static/*',
cache({
cacheName: 'my-app',
cacheControl: 'max-age=3600',
})
)
Built-in Middleware Reference: See references/middleware-catalog.md
Middleware Chaining
app.get(
'/protected',
authMiddleware,
rateLimitMiddleware,
(c) => {
return c.json({ data: 'Protected data' })
}
)
const authMiddleware = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
c.set('user', { id: 1, name: 'Alice' })
await next()
}
const rateLimitMiddleware = async (c, next) => {
await next()
}
Why Chain Middleware:
- Separation of concerns
- Reusable across routes
- Clear execution order
Custom Middleware
const timing = async (c, next) => {
const start = Date.now()
await next()
const elapsed = Date.now() - start
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
}
const requestId = async (c, next) => {
const id = crypto.randomUUID()
c.set('requestId', id)
await next()
c.res.headers.set('X-Request-ID', id)
}
const errorLogger = async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
}
}
app.use('*', timing)
app.use('*', requestId)
app.use('*', errorLogger)
Best Practices:
- Keep middleware focused (single responsibility)
- Use
c.set() to share data between middleware
- Check
c.error AFTER next() for error handling
- Return early to short-circuit execution
Part 3: Type-Safe Context Extension
Using c.set() and c.get()
import { Hono } from 'hono'
type Bindings = {
DATABASE_URL: string
}
type Variables = {
user: {
id: number
name: string
}
requestId: string
}
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
app.use('/api/*', async (c, next) => {
c.set('user', { id: 1, name: 'Alice' })
await next()
})
app.get('/api/profile', (c) => {
const user = c.get('user')
const requestId = c.get('requestId')
return c.json({ user, requestId })
})
CRITICAL:
- Define
Variables type for type-safe c.get()
- Define
Bindings type for environment variables (Cloudflare Workers)
c.set() in middleware, c.get() in handlers
Custom Context Extension
import { Hono } from 'hono'
import type { Context } from 'hono'
type Env = {
Variables: {
logger: {
info: (message: string) => void
error: (message: string) => void
}
}
}
const app = new Hono<Env>()
app.use('*', async (c, next) => {
const logger = {
info: (msg: string) => console.log(`[INFO] ${msg}`),
error: (msg: string) => console.error(`[ERROR] ${msg}`),
}
c.set('logger', logger)
await next()
})
app.get('/', (c) => {
const logger = c.get('logger')
logger.info('Hello from route')
return c.json({ message: 'Hello' })
})
Advanced Pattern: See templates/context-extension.ts
Part 4: Request Validation
Validation with Zod
npm install zod@4.1.12 @hono/zod-validator@0.7.4
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional(),
})
app.post('/users', zValidator('json', userSchema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
const searchSchema = z.object({
q: z.string(),
page: z.string().transform((val) => parseInt(val, 10)),
limit: z.string().transform((val) => parseInt(val, 10)).optional(),
})
app.get('/search', zValidator('query', searchSchema), (c) => {
const { q, page, limit } = c.req.valid('query')
return c.json({ q, page, limit })
})
const idSchema = z.object({
id: z.string().uuid(),
})
app.get('/users/:id', zValidator('param', idSchema), (c) => {
const { id } = c.req.valid('param')
return c.json({ userId: id })
})
const headerSchema = z.object({
'authorization': z.string().startsWith('Bearer '),
'content-type': z.string(),
})
app.post('/auth', zValidator('header', headerSchema), (c) => {
const headers = c.req.valid('header')
return c.json({ authenticated: true })
})
CRITICAL:
- Always use
c.req.valid() after validation (type-safe)
- Validation targets:
json, query, param, header, form, cookie
- Use
z.transform() to convert strings to numbers/dates
- Validation errors return 400 automatically
Custom Validation Hooks
import { zValidator } from '@hono/zod-validator'
import { HTTPException } from 'hono/http-exception'
const schema = z.object({
name: z.string(),
age: z.number(),
})
app.post(
'/users',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
issues: result.error.issues,
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
app.post(
'/users',
zValidator('json', schema, (result, c) => {
if (!result.success) {
throw new HTTPException(400, { cause: result.error })
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
Validation with Valibot
npm install valibot@1.1.0 @hono/valibot-validator@0.5.3
import { vValidator } from '@hono/valibot-validator'
import * as v from 'valibot'
const schema = v.object({
name: v.string(),
age: v.number(),
})
app.post('/users', vValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
Zod vs Valibot: See references/validation-libraries.md
Validation with Typia
npm install typia @hono/typia-validator@0.1.2
import { typiaValidator } from '@hono/typia-validator'
import typia from 'typia'
interface User {
name: string
age: number
}
const validate = typia.createValidate<User>()
app.post('/users', typiaValidator('json', validate), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
Why Typia:
- Fastest validation (compile-time)
- No runtime schema definition
- AOT (Ahead-of-Time) compilation
Validation with ArkType
npm install arktype @hono/arktype-validator@2.0.1
import { arktypeValidator } from '@hono/arktype-validator'
import { type } from 'arktype'
const schema = type({
name: 'string',
age: 'number',
})
app.post('/users', arktypeValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
Comparison: See references/validation-libraries.md for detailed comparison
Part 5: Typed Routes (RPC)
Why RPC?
Hono's RPC feature allows type-safe client/server communication without manual API type definitions. The client infers types directly from the server routes.
Server-Side Setup
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const schema = z.object({
name: z.string(),
age: z.number(),
})
const route = app.post(
'/users',
zValidator('json', schema),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data }, 201)
}
)
export type AppType = typeof route
export default app
CRITICAL:
- Must use
const route = app.get(...) for RPC type inference
- Export
typeof route or typeof app
- Don't use anonymous route definitions
Client-Side Setup
import { hc } from 'hono/client'
import type { AppType } from './app'
const client = hc<AppType>('http://localhost:8787')
const res = await client.users.$post({
json: {
name: 'Alice',
age: 30,
},
})
const data = await res.json()
Why RPC:
- ✅ Full type inference (request + response)
- ✅ No manual type definitions
- ✅ Compile-time error checking
- ✅ Auto-complete in IDE
RPC with Multiple Routes
const app = new Hono()
const getUsers = app.get('/users', (c) => {
return c.json({ users: [] })
})
const createUser = app.post(
'/users',
zValidator('json', userSchema),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data }, 201)
}
)
const getUser = app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'Alice' })
})
export type AppType = typeof getUsers | typeof createUser | typeof getUser
const client = hc<AppType>('http://localhost:8787')
const usersRes = await client.users.$get()
const createRes = await client.users.$post({
json: { name: 'Alice', age: 30 },
})
const userRes = await client.users[':id'].$get({
param: { id: '123' },
})
RPC Performance Optimization
Problem: Large apps with many routes cause slow type inference
Solution: Export specific route groups instead of entire app
export type AppType = typeof app
const userRoutes = app.get('/users', ...).post('/users', ...)
export type UserRoutes = typeof userRoutes
const postRoutes = app.get('/posts', ...).post('/posts', ...)
export type PostRoutes = typeof postRoutes
import type { UserRoutes } from './app'
const userClient = hc<UserRoutes>('http://localhost:8787')
Deep Dive: See references/rpc-guide.md
Part 6: Error Handling
HTTPException
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
app.get('/users/:id', (c) => {
const id = c.req.param('id')
if (!id) {
throw new HTTPException(400, { message: 'ID is required' })
}
if (id === 'invalid') {
const res = new Response('Custom error body', { status: 400 })
throw new HTTPException(400, { res })
}
return c.json({ id })
})
CRITICAL:
- Use HTTPException for expected errors (400, 401, 403, 404)
- Don't use for unexpected errors (500) - use
onError instead
- HTTPException stops execution immediately
Global Error Handler (onError)
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error('Unexpected error:', err)
return c.json(
{
error: 'Internal Server Error',
message: err.message,
},
500
)
})
app.get('/error', (c) => {
throw new Error('Something went wrong!')
})
Why onError:
- Centralized error handling
- Consistent error responses
- Error logging and tracking
Middleware Error Checking
app.use('*', async (c, next) => {
await next()
if (c.error) {
console.error('Error in route:', c.error)
}
})
Not Found Handler
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
Error Handling Best Practices
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
app.post('/users', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ data })
})
app.use('/admin/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
await next()
})
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await db.getUser(id)
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json({ user })
})
app.get('/data', async (c) => {
try {
const data = await fetchExternalAPI()
return c.json({ data })
} catch (error) {
throw error
}
})
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error('Unexpected error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
})
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
Critical Rules
Always Do
✅ Call await next() in middleware - Required for middleware chain execution
✅ Return Response from handlers - Use c.json(), c.text(), c.html()
✅ Use c.req.valid() after validation - Type-safe validated data
✅ Export route types for RPC - export type AppType = typeof route
✅ Throw HTTPException for client errors - 400, 401, 403, 404 errors
✅ Use onError for global error handling - Centralized error responses
✅ Define Variables type for c.set/c.get - Type-safe context variables
✅ Use const route = app.get(...) - Required for RPC type inference
Never Do
❌ Forget await next() in middleware - Breaks middleware chain
❌ Use res.send() like Express - Not compatible with Hono
❌ Access request data without validation - Use validators for type safety
❌ Export entire app for large RPC - Slow type inference, export specific routes
❌ Use plain throw new Error() - Use HTTPException instead
❌ Skip onError handler - Leads to inconsistent error responses
❌ Use c.set/c.get without Variables type - Loses type safety
Known Issues Prevention
This skill prevents 8 documented issues:
Issue #1: RPC Type Inference Slow
Error: IDE becomes slow with many routes
Source: hono/docs/guides/rpc
Why It Happens: Complex type instantiation from typeof app with many routes
Prevention: Export specific route groups instead of entire app
export type AppType = typeof app
const userRoutes = app.get(...).post(...)
export type UserRoutes = typeof userRoutes
Issue #2: Middleware Response Not Typed in RPC
Error: Middleware responses not inferred by RPC client
Source: honojs/hono#2719
Why It Happens: RPC mode doesn't infer middleware responses by default
Prevention: Export specific route types that include middleware
const route = app.get(
'/data',
myMiddleware,
(c) => c.json({ data: 'value' })
)
export type AppType = typeof route
Issue #3: Validation Hook Confusion
Error: Different validator libraries have different hook patterns
Source: Context7 research
Why It Happens: Each validator (@hono/zod-validator, @hono/valibot-validator, etc.) has slightly different APIs
Prevention: This skill provides consistent patterns for all validators
Issue #4: HTTPException Misuse
Error: Throwing plain Error instead of HTTPException
Source: Official docs
Why It Happens: Developers familiar with Express use throw new Error()
Prevention: Always use HTTPException for client errors (400-499)
throw new Error('Unauthorized')
throw new HTTPException(401, { message: 'Unauthorized' })
Issue #5: Context Type Safety Lost
Error: c.set() and c.get() without type inference
Source: Official docs
Why It Happens: Not defining Variables type in Hono generic
Prevention: Always define Variables type
type Variables = {
user: { id: number; name: string }
}
const app = new Hono<{ Variables: Variables }>()
Issue #6: Missing Error Check After Middleware
Error: Errors in handlers not caught
Source: Official docs
Why It Happens: Not checking c.error after await next()
Prevention: Check c.error in middleware
app.use('*', async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
}
})
Issue #7: Direct Request Access Without Validation
Error: Accessing c.req.param() or c.req.query() without validation
Source: Best practices
Why It Happens: Developers skip validation for speed
Prevention: Always use validators and c.req.valid()
const id = c.req.param('id')
app.get('/users/:id', zValidator('param', idSchema), (c) => {
const { id } = c.req.valid('param')
})
Issue #8: Incorrect Middleware Order
Error: Middleware executing in wrong order
Source: Official docs
Why It Happens: Misunderstanding middleware chain execution
Prevention: Remember middleware runs top-to-bottom, await next() runs handler, then bottom-to-top
app.use('*', async (c, next) => {
console.log('1: Before handler')
await next()
console.log('4: After handler')
})
app.use('*', async (c, next) => {
console.log('2: Before handler')
await next()
console.log('3: After handler')
})
app.get('/', (c) => {
console.log('Handler')
return c.json({})
})
Configuration Files Reference
package.json (Full Example)
{
"name": "hono-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"hono": "^4.10.2"
},
"devDependencies": {
"typescript": "^5.9.0",
"tsx": "^4.19.0",
"@types/node": "^22.10.0"
}
}
package.json with Validation (Zod)
{
"dependencies": {
"hono": "^4.10.2",
"zod": "^4.1.12",
"@hono/zod-validator": "^0.7.4"
}
}
package.json with Validation (Valibot)
{
"dependencies": {
"hono": "^4.10.2",
"valibot": "^1.1.0",
"@hono/valibot-validator": "^0.5.3"
}
}
package.json with All Validators
{
"dependencies": {
"hono": "^4.10.2",
"zod": "^4.1.12",
"valibot": "^1.1.0",
"@hono/zod-validator": "^0.7.4",
"@hono/valibot-validator": "^0.5.3",
"@hono/typia-validator": "^0.1.2",
"@hono/arktype-validator": "^2.0.1"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
File Templates
All templates are available in the templates/ directory:
- routing-patterns.ts - Route params, query params, wildcards, grouping
- middleware-composition.ts - Middleware chaining, built-in middleware
- validation-zod.ts - Zod validation with custom hooks
- validation-valibot.ts - Valibot validation
- rpc-pattern.ts - Type-safe RPC client/server
- error-handling.ts - HTTPException, onError, custom errors
- context-extension.ts - c.set/c.get, custom context types
- package.json - All dependencies
Copy these files to your project and customize as needed.
Reference Documentation
For deeper understanding, see:
- middleware-catalog.md - Complete built-in Hono middleware reference
- validation-libraries.md - Zod vs Valibot vs Typia vs ArkType comparison
- rpc-guide.md - RPC pattern deep dive, performance optimization
- top-errors.md - Common Hono errors with solutions
Official Documentation
Dependencies (Latest Verified 2025-10-22)
{
"dependencies": {
"hono": "^4.10.2"
},
"optionalDependencies": {
"zod": "^4.1.12",
"valibot": "^1.1.0",
"@hono/zod-validator": "^0.7.4",
"@hono/valibot-validator": "^0.5.3",
"@hono/typia-validator": "^0.1.2",
"@hono/arktype-validator": "^2.0.1"
},
"devDependencies": {
"typescript": "^5.9.0"
}
}
Production Example
This skill is validated across multiple runtime environments:
- Cloudflare Workers: Routing, middleware, RPC patterns
- Deno: All validation libraries tested
- Bun: Performance benchmarks completed
- Node.js: Full test suite passing
All patterns in this skill have been validated in production.
Questions? Issues?
- Check
references/top-errors.md first
- Verify all steps in the setup process
- Ensure
await next() is called in middleware
- Ensure RPC routes use
const route = app.get(...) pattern
- Check official docs: https://hono.dev