hono-routing

Build type-safe APIs with Hono for Cloudflare Workers, Deno, Bun, and Node.js. Supports routing, middleware, validation (Zod/Valibot), RPC, streaming (SSE), WebSocket, and security features (CSRF, secure headers) Type-safe context extension with c.set() and c.get() , full TypeScript inference for request/response data RPC pattern enables type-safe client/server communication without manual API definitions; export route types for instant IDE autocomplete Prevents 10 documented issues including RPC type inference slowdown, middleware response typing, JWT algorithm requirements (v4.11.4+), and request body consumption errors

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill hono-routing
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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<string, Response>()

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)

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<Env>()

// 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<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

// 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<AppType>('http://localhost:8787')

// 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<AppType>('http://localhost:8787')

// 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<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')

// 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](https://hono.dev/docs/guides/rpc) | [GitHub Issue #3869](https://github.com/honojs/hono/issues/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](https://github.com/honojs/hono/issues/2719) | [GitHub Issue #4600](https://github.com/honojs/hono/issues/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<typeof app>('http://localhost:8787')

const res = await client.users[':id'].$get({ param: { id: '123' } })

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](https://github.com/honojs/hono/issues/4625) | [Security Advisory GHSA-f67f-6cw9-8mq4](https://github.com/honojs/hono/security/advisories/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](https://github.com/honojs/hono/issues/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](https://hono.dev)

- **Hono Routing**: [https://hono.dev/docs/api/routing](https://hono.dev/docs/api/routing)

- **Hono Middleware**: [https://hono.dev/docs/guides/middleware](https://hono.dev/docs/guides/middleware)

- **Hono Validation**: [https://hono.dev/docs/guides/validation](https://hono.dev/docs/guides/validation)

- **Hono RPC**: [https://hono.dev/docs/guides/rpc](https://hono.dev/docs/guides/rpc)

- **Hono Context**: [https://hono.dev/docs/api/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"

}

}

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card