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.
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
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
// app.tsimport { Hono } from'hono'import { zValidator } from'@hono/zod-validator'import { z } from'zod'const app = newHono()
const schema = z.object({
name: z.string(),
age: z.number(),
})
// Define route and export typeconst route = app.post(
'/users',
zValidator('json', schema),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data }, 201)
}
)
// Export app type for RPC clientexporttypeAppType = typeof route
// OR export entire app// export type AppType = typeof appexportdefault 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
// client.tsimport { hc } from'hono/client'importtype { AppType } from'./app'const client = hc<AppType>('http://localhost:8787')
// Type-safe API callconst res = await client.users.$post({
json: {
name: 'Alice',
age: 30,
},
})
// Response is typed!const data = await res.json() // { success: boolean, data: { name: string, age: number } }
import { Hono } from'hono'import { HTTPException } from'hono/http-exception'const app = newHono()
// Validation errors
app.post('/users', zValidator('json', schema), (c) => {
// zValidator automatically returns 400 on validation failureconst data = c.req.valid('json')
return c.json({ data })
})
// Authorization errors
app.use('/admin/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
thrownewHTTPException(401, { message: 'Unauthorized' })
}
awaitnext()
})
// Not found errors
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await db.getUser(id)
if (!user) {
thrownewHTTPException(404, { message: 'User not found' })
}
return c.json({ user })
})
// Server errors
app.get('/data', async (c) => {
try {
const data = awaitfetchExternalAPI()
return c.json({ data })
} catch (error) {
// Let onError handle itthrow error
}
})
// Global error handler
app.onError((err, c) => {
if (err instanceofHTTPException) {
return err.getResponse()
}
console.error('Unexpected error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
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/rpcWhy It Happens: Complex type instantiation from typeof app with many routes
Prevention: Export specific route groups instead of entire app
Error: Middleware responses not inferred by RPC client
Source: honojs/hono#2719Why It Happens: RPC mode doesn't infer middleware responses by default
Prevention: Export specific route types that include middleware
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)
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
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
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()
// ❌ Wrongconst id = c.req.param('id') // string, no validation// ✅ Correct
app.get('/users/:id', zValidator('param', idSchema), (c) => {
const { id } = c.req.valid('param') // validated UUID
})
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