Hono Routing & Middleware
Status: Production Ready ✅ Last Updated: 2026-01-20 Dependencies: None (framework-agnostic) Latest Versions: hono@4.11.4, zod@4.3.5, valibot@1.2.0, @hono/zod-validator@0.7.6, @hono/valibot-validator@0.6.1
Quick Start (15 Minutes) 1. Install Hono npm install hono@4.11.4
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.3.5 @hono/zod-validator@0.7.6
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 4-Part Hono Mastery Guide Part 1: Routing Patterns Route Parameters // Single parameter app.get('/users/:id', (c) => { const id = c.req.param('id') return c.json({ userId: id }) })
// Multiple parameters app.get('/posts/:postId/comments/:commentId', (c) => { const { postId, commentId } = c.req.param() return c.json({ postId, commentId }) })
// Optional parameters (using wildcards) 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) Route Parameter Regex Constraints
Use regex patterns in routes to restrict parameter matching at the routing level:
// Only matches numeric IDs app.get('/users/:id{[0-9]+}', (c) => { const id = c.req.param('id') // Guaranteed to be digits return c.json({ userId: id }) })
// Only matches UUIDs app.get('/posts/:id{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}', (c) => { const id = c.req.param('id') // Guaranteed to be UUID format return c.json({ postId: id }) })
Benefits:
Early validation at routing level Prevents invalid requests from reaching handlers Self-documenting route constraints Query Parameters app.get('/search', (c) => { // Single query param const q = c.req.query('q')
// Multiple query params const { page, limit } = c.req.query()
// Query param array (e.g., ?tag=js&tag=ts) 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 Route Grouping (Sub-apps) // Create sub-app const api = new Hono()
api.get('/users', (c) => c.json({ users: [] })) api.get('/posts', (c) => c.json({ posts: [] }))
// Mount sub-app const app = new Hono() app.route('/api', api)
// Result: /api/users, /api/posts
Why Group Routes:
Organize large applications Share middleware for specific routes Better code structure and maintainability Part 2: Middleware & Validation
CRITICAL Middleware Rule:
Always call await next() in middleware to continue the chain Return early (without calling next()) to prevent handler execution Check c.error AFTER next() for error handling app.use('/admin/*', async (c, next) => { const token = c.req.header('Authorization') if (!token) return c.json({ error: 'Unauthorized' }, 401) await next() // Required! })
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()
// Request logging app.use('*', logger())
// CORS app.use('/api/*', cors({ origin: 'https://example.com', allowMethods: ['GET', 'POST', 'PUT', 'DELETE'], allowHeaders: ['Content-Type', 'Authorization'], }))
// Pretty JSON (dev only) app.use('*', prettyJSON())
// Compression (gzip/deflate) app.use('*', compress())
// Cache responses app.use( '/static/*', cache({ cacheName: 'my-app', cacheControl: 'max-age=3600', }) )
Custom Cache Middleware Pattern:
When implementing custom cache middleware for Node.js (or other non-Cloudflare runtimes), you must clone responses before storing them in cache:
const cache = new Map
const customCache = async (c, next) => { const key = c.req.url
// Check cache const cached = cache.get(key) if (cached) { return cached.clone() // Clone when returning from cache }
// Execute handler await next()
// Store in cache (must clone!) cache.set(key, c.res.clone()) // ✅ Clone before storing }
app.use('*', customCache)
Why Cloning is Required: Response bodies are readable streams that can only be consumed once. Cloning creates a new response with a fresh stream.
Built-in Middleware Reference: See references/middleware-catalog.md
Streaming Helpers (SSE, AI Responses)
```typescript import { Hono } from 'hono' import { stream, streamText, streamSSE } from 'hono/streaming'
const app = new Hono()
// Binary streaming app.get('/download', (c) => { return stream(c, async (stream) => { await stream.write(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])) await stream.pipe(readableStream) }) })
// Text streaming (AI responses) app.get('/ai', (c) => { return streamText(c, async (stream) => { for await (const chunk of aiResponse) { await stream.write(chunk) await stream.sleep(50) // Rate limit if needed } }) })
// Server-Sent Events (real-time updates) app.get('/sse', (c) => { return streamSSE(c, async (stream) => { let id = 0 while (true) { await stream.writeSSE({ data: JSON.stringify({ time: Date.now() }), event: 'update', id: String(id++), }) await stream.sleep(1000) } }) })
Use Cases:
stream() - Binary files, video, audio streamText() - AI chat responses, typewriter effects streamSSE() - Real-time notifications, live feeds WebSocket Helper import { Hono } from 'hono' import { upgradeWebSocket } from 'hono/cloudflare-workers' // Platform-specific!
const app = new Hono()
app.get('/ws', upgradeWebSocket((c) => ({
onMessage(event, ws) {
console.log(Message: ${event.data})
ws.send(Echo: ${event.data})
},
onClose: () => console.log('Closed'),
onError: (event) => console.error('Error:', event),
// onOpen is NOT supported on Cloudflare Workers!
})))
export default app
⚠️ Cloudflare Workers WebSocket Caveats:
Import from hono/cloudflare-workers (not hono/ws) onOpen callback is NOT supported (Cloudflare limitation) CORS/header-modifying middleware conflicts with WebSocket routes Use route grouping to exclude WebSocket routes from CORS: const api = new Hono() api.use('*', cors()) // CORS for API only app.route('/api', api) app.get('/ws', upgradeWebSocket(...)) // No CORS on WebSocket
Security Middleware import { Hono } from 'hono' import { secureHeaders } from 'hono/secure-headers' import { csrf } from 'hono/csrf'
const app = new Hono()
// Security headers (X-Frame-Options, CSP, HSTS, etc.) app.use('*', secureHeaders({ xFrameOptions: 'DENY', xXssProtection: '1; mode=block', contentSecurityPolicy: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], }, }))
// CSRF protection (validates Origin header) app.use('/api/*', csrf({ origin: ['https://example.com', 'https://admin.example.com'], }))
Security Middleware Options:
Middleware Purpose secureHeaders X-Frame-Options, CSP, HSTS, XSS protection csrf CSRF via Origin/Sec-Fetch-Site validation bearerAuth Bearer token authentication basicAuth HTTP Basic authentication ipRestriction IP allowlist/blocklist Combine Middleware
Compose middleware with conditional logic:
import { Hono } from 'hono' import { some, every, except } from 'hono/combine' import { bearerAuth } from 'hono/bearer-auth' import { ipRestriction } from 'hono/ip-restriction'
const app = new Hono()
// some: ANY middleware must pass (OR logic) app.use('/admin/*', some( bearerAuth({ token: 'admin-token' }), ipRestriction({ allowList: ['10.0.0.0/8'] }), ))
// every: ALL middleware must pass (AND logic) app.use('/secure/*', every( bearerAuth({ token: 'secret' }), ipRestriction({ allowList: ['192.168.1.0/24'] }), ))
// except: Skip middleware for certain paths app.use('*', except( ['/health', '/metrics'], logger(), ))
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 }>()
// Middleware sets 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() })
// Route accesses variables app.get('/api/profile', (c) => { const user = c.get('user') // Type-safe! const requestId = c.get('requestId') // Type-safe!
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
// Create logger middleware
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.3.5 @hono/zod-validator@0.7.6
import { zValidator } from '@hono/zod-validator' import { z } from 'zod'
// Define schema const userSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().min(18).optional(), })
// Validate JSON body app.post('/users', zValidator('json', userSchema), (c) => { const data = c.req.valid('json') // Type-safe! return c.json({ success: true, data }) })
// Validate query params 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 }) })
// Validate route params 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 }) })
// Validate headers 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
⚠️ CRITICAL: Validation Must Be Handler-Specific
For validated types to be inferred correctly, validation middleware must be added in the handler, not via app.use():
// ❌ WRONG - Type inference breaks app.use('/users', zValidator('json', userSchema))
app.post('/users', (c) => { const data = c.req.valid('json') // TS Error: Type 'never' return c.json({ data }) })
// ✅ CORRECT - Validation in handler app.post('/users', zValidator('json', userSchema), (c) => { const data = c.req.valid('json') // Type-safe! return c.json({ data }) })
Why It Happens: Hono's Input type mapping merges validation results using generics. When validators are applied via app.use(), the type system cannot track which routes have which validation schemas, causing the Input generic to collapse to never.
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(), })
// Custom error handler app.post( '/users', zValidator('json', schema, (result, c) => { if (!result.success) { // Custom error response return c.json( { error: 'Validation failed', issues: result.error.issues, }, 400 ) } }), (c) => { const data = c.req.valid('json') return c.json({ success: true, data }) } )
// Throw HTTPException 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 }) } )
Note on Zod Optional Enums: Prior to @hono/zod-validator@0.7.6, optional enums incorrectly resolved to strings instead of the enum type. This was fixed in v0.7.6. Ensure you're using the latest version:
npm install @hono/zod-validator@0.7.6
Validation with Valibot npm install valibot@1.2.0 @hono/valibot-validator@0.6.1
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
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 // app.ts 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(), })
// Define route and export type const 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 client export type AppType = typeof route
// OR export entire app // export type AppType = typeof app
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 // client.ts import { hc } from 'hono/client' import type { AppType } from './app'
const client = hc
// Type-safe API call const 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 } }
Why RPC:
✅ Full type inference (request + response) ✅ No manual type definitions ✅ Compile-time error checking ✅ Auto-complete in IDE
⚠️ RPC Type Inference Limitation: The RPC client only infers types for json and text responses. If an endpoint returns multiple response types (e.g., JSON and binary), none of the responses will be type-inferred:
// ❌ Type inference fails - mixes JSON and binary app.post('/upload', async (c) => { const body = await c.req.body() // Binary response if (error) { return c.json({ error: 'Bad request' }, 400) // JSON response } return c.json({ success: true }) })
// ✅ Separate endpoints by response type app.post('/upload', async (c) => { return c.json({ success: true }) // Only JSON - types work })
app.get('/download/:id', async (c) => { return c.body(binaryData) // Only binary - separate endpoint })
RPC with Multiple Routes // Server 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 combined type export type AppType = typeof getUsers | typeof createUser | typeof getUser
// Client
const client = hc
// GET /users const usersRes = await client.users.$get()
// POST /users const createRes = await client.users.$post({ json: { name: 'Alice', age: 30 }, })
// GET /users/:id 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
// ❌ Slow: Export entire app export type AppType = typeof app
// ✅ Fast: Export specific routes const userRoutes = app.get('/users', ...).post('/users', ...) export type UserRoutes = typeof userRoutes
const postRoutes = app.get('/posts', ...).post('/posts', ...) export type PostRoutes = typeof postRoutes
// Client imports specific routes
import type { UserRoutes } from './app'
const userClient = hc
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')
// Throw HTTPException for client errors if (!id) { throw new HTTPException(400, { message: 'ID is required' }) }
// With custom response 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()
// Custom error handler app.onError((err, c) => { // Handle HTTPException if (err instanceof HTTPException) { return err.getResponse() }
// Handle unexpected errors 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()
// Check for errors after handler if (c.error) { console.error('Error in route:', c.error) // Send to error tracking service } })
Not Found 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 10 documented issues:
Issue #1: RPC Type Inference Slow
Error: IDE becomes slow with many routes (8-minute CI builds, non-existent IntelliSense) Source: hono/docs/guides/rpc | GitHub Issue #3869 Why It Happens: Complex type instantiation from typeof app with many routes. Exacerbated by Zod methods like omit, extend, pick. Prevention: Export specific route groups instead of entire app
// ❌ Slow export type AppType = typeof app
// ✅ Fast const userRoutes = app.get(...).post(...) export type UserRoutes = typeof userRoutes
Advanced Workaround for Large Apps (100+ routes):
Split into monorepo libs: // routers-auth/index.ts export const authRouter = new Hono() .get('/login', ...) .post('/login', ...)
// routers-orders/index.ts export const orderRouter = new Hono() .get('/orders', ...) .post('/orders', ...)
// routers-main/index.ts const app = new Hono() .route('/auth', authRouter) .route('/orders', orderRouter)
export type AppType = typeof app
Use separate build configs:
Production: Full tsc with .d.ts generation (for RPC client) Development: Skip tsc on main router, only type-check sub-routers (faster live-reload)
Avoid Zod methods that hurt performance:
z.omit(), z.extend(), z.pick() - These increase language server workload by 10x Use interfaces instead of intersections when possible Issue #2: Middleware Response Not Typed in RPC
Error: Middleware responses (including notFound() and onError()) not inferred by RPC client Source: honojs/hono#2719 | GitHub Issue #4600 Why It Happens: RPC mode doesn't infer middleware responses by default. Responses from notFound() or onError() handlers are not included in type map. Prevention: Export specific route types that include middleware
const route = app.get( '/data', myMiddleware, (c) => c.json({ data: 'value' }) ) export type AppType = typeof route
Specific Issue: notFound/onError Not Typed:
// Server const app = new Hono() .notFound((c) => c.json({ error: 'Not Found' }, 404)) .get('/users/:id', async (c) => { const user = await getUser(c.req.param('id')) if (!user) { return c.notFound() // Type not exported to RPC client } return c.json({ user }) })
// Client
const client = hc
if (res.status === 404) { const error = await res.json() // Type is 'any', not { error: string } }
Partial Workaround (v4.11.0+): Use module augmentation to customize NotFoundResponse type:
import { Hono, TypedResponse } from 'hono'
declare module 'hono' { interface NotFoundResponse extends Response, TypedResponse<{ error: string }, 404, 'json'> {} }
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)
// ❌ Wrong throw new Error('Unauthorized')
// ✅ Correct 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()
// ❌ Wrong const 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
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({}) })
// Output: 1, 2, Handler, 3, 4
Issue #9: JWT verify() Requires Algorithm Parameter (v4.11.4+)
Error: TypeError: Cannot read properties of undefined Source: GitHub Issue #4625 | Security Advisory GHSA-f67f-6cw9-8mq4 Why It Happens: Security fix in v4.11.4 requires explicit algorithm specification to prevent JWT header manipulation Prevention: Always specify the algorithm parameter
import { verify } from 'hono/jwt'
// ❌ Wrong (pre-v4.11.4 syntax) const payload = await verify(token, secret)
// ✅ Correct (v4.11.4+) const payload = await verify(token, secret, 'HS256') // Algorithm required
Note: This was a breaking change released in a patch version due to security severity. Update all JWT verification code when upgrading to v4.11.4+.
Issue #10: Request Body Consumed by Middleware
Error: TypeError: Body is unusable Source: GitHub Issue #4259 Why It Happens: Using c.req.raw.clone() bypasses Hono's cache and consumes the body stream Prevention: Always use c.req.text() or c.req.json() instead of accessing raw request
// ❌ Wrong - Breaks downstream validators app.use('*', async (c, next) => { const body = await c.req.raw.clone().text() // Consumes body! console.log('Request body:', body) await next() })
app.post('/', zValidator('json', schema), async (c) => { const data = c.req.valid('json') // Error: Body is unusable return c.json({ data }) })
// ✅ Correct - Uses cached content app.use('*', async (c, next) => { const body = await c.req.text() // Cache-friendly console.log('Request body:', body) await next() })
app.post('/', zValidator('json', schema), async (c) => { const data = c.req.valid('json') // Works! return c.json({ data }) })
Why: Request bodies in Web APIs can only be read once (they're streams). Hono's validator internally uses await c.req.json() which caches the content. If you use c.req.raw.clone().json(), it bypasses the cache and consumes the body, causing subsequent reads to fail.
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.11.4" }, "devDependencies": { "typescript": "^5.9.0", "tsx": "^4.19.0", "@types/node": "^22.10.0" } }
package.json with Validation (Zod) { "dependencies": { "hono": "^4.11.4", "zod": "^4.3.5", "@hono/zod-validator": "^0.7.6" } }
package.json with Validation (Valibot) { "dependencies": { "hono": "^4.11.4", "valibot": "^1.2.0", "@hono/valibot-validator": "^0.6.1" } }
package.json with All Validators { "dependencies": { "hono": "^4.11.4", "zod": "^4.3.5", "valibot": "^1.2.0", "@hono/zod-validator": "^0.7.6", "@hono/valibot-validator": "^0.6.1", "@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 Hono: https://hono.dev Hono Routing: https://hono.dev/docs/api/routing Hono Middleware: https://hono.dev/docs/guides/middleware Hono Validation: https://hono.dev/docs/guides/validation Hono RPC: https://hono.dev/docs/guides/rpc Hono Context: https://hono.dev/docs/api/context Context7 Library ID: /llmstxt/hono_dev_llms-full_txt Dependencies (Latest Verified 2026-01-20) { "dependencies": { "hono": "^4.11.4" }, "optionalDependencies": { "zod": "^4.3.5", "valibot": "^1.2.0", "@hono/zod-validator": "^0.7.6", "@hono/valibot-validator": "^0.6.1", "@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