| name | hono-routing |
| description | Build type-safe APIs with Hono for Cloudflare Workers, Deno, Bun, Node.js. Routing, middleware, validation (Zod/Valibot), RPC, streaming (SSE), WebSocket, security (CSRF, secureHeaders). |
| license | MIT |
| compatibility | agents |
| metadata | {"audience":"backend-developers, full-stack-engineers, api-developers","domain":"api-development, backend-development"} |
Hono Routing & Middleware
What I do
Provide production-ready patterns for building type-safe APIs with Hono, including routing, middleware chains, request validation, RPC patterns, and advanced features like streaming and WebSocket support.
When to use me
Use this skill when:
- Building Hono APIs for any JavaScript runtime
- Implementing request validation with Zod/Valibot
- Setting up type-safe RPC client/server patterns
- Creating streaming endpoints (SSE, AI responses)
- Implementing WebSocket connections
- Configuring security middleware (CSRF, headers)
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
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)
Route Parameter Regex Constraints
Use regex patterns in routes to restrict parameter matching at the routing level:
app.get('/users/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
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')
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) => {
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
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 & 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()
})
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',
})
)
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<string, Response>()
const customCache = async (c, next) => {
const key = c.req.url
const cached = cache.get(key)
if (cached) {
return cached.clone()
}
await next()
cache.set(key, c.res.clone())
}
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'
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),
})))
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())
app.route('/api', api)
app.get('/ws', upgradeWebSocket(...))
Security Middleware
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
import { csrf } from 'hono/csrf'
const app = new Hono()
app.use('*', secureHeaders({
xFrameOptions: 'DENY',
xXssProtection: '1; mode=block',
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
},
}))
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()
app.use('/admin/*', some(
bearerAuth({ token: 'admin-token' }),
ipRestriction({ allowList: ['10.0.0.0/8'] }),
))
app.use('/secure/*', every(
bearerAuth({ token: 'secret' }),
ipRestriction({ allowList: ['192.168.1.0/24'] }),
))
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 }>()
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.3.5 @hono/zod-validator@0.7.6
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
⚠️ 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():
app.use('/users', zValidator('json', userSchema))
app.post('/users', (c) => {
const data = c.req.valid('json')
return c.json({ data })
})
app.post('/users', zValidator('json', userSchema), (c) => {
const data = c.req.valid('json')
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(),
})
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 })
}
)
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<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 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:
app.post('/upload', async (c) => {
const body = await c.req.body()
if (error) {
return c.json({ error: 'Bad request' }, 400)
}
return c.json({ success: true })
})
app.post('/upload', async (c) => {
return c.json({ success: true })
})
app.get('/download/:id', async (c) => {
return c.body(binaryData)
})
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)
})
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
export type AppType = typeof app
const userRoutes = app.get(...).post(...)
export type UserRoutes = typeof userRoutes
Advanced Workaround for Large Apps (100+ routes):
- Split into monorepo libs:
export const authRouter = new Hono()
.get('/login', ...)
.post('/login', ...)
export const orderRouter = new Hono()
.get('/orders', ...)
.post('/orders', ...)
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:
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()
}
return c.json({ user })
})
const client = hc<typeof app>('http://localhost:8787')
const res = await client.users[':id'].$get({ param: { id: '123' } })
if (res.status === 404) {
const error = await res.json()
}
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)
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({})
})
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'
const payload = await verify(token, secret)
const payload = await verify(token, secret, 'HS256')
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
app.use('*', async (c, next) => {
const body = await c.req.raw.clone().text()
console.log('Request body:', body)
await next()
})
app.post('/', zValidator('json', schema), async (c) => {
const data = c.req.valid('json')
return c.json({ data })
})
app.use('*', async (c, next) => {
const body = await c.req.text()
console.log('Request body:', body)
await next()
})
app.post('/', zValidator('json', schema), async (c) => {
const data = c.req.valid('json')
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
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