Claude-skill-registry hono-routing

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hono-routing-dennislee928-smart-zone" ~/.claude/skills/majiayu000-claude-skill-registry-hono-routing && rm -rf "$T"
manifest: skills/data/hono-routing-dennislee928-smart-zone/SKILL.md
source content

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)

```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:

MiddlewarePurpose
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 | 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):

  1. 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
  1. 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)
  2. 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<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 | 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


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?

  1. Check
    references/top-errors.md
    first
  2. Verify all steps in the setup process
  3. Ensure
    await next()
    is called in middleware
  4. Ensure RPC routes use
    const route = app.get(...)
    pattern
  5. Check official docs: https://hono.dev