**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 * * * ```
npm install hono@4.11.4import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => { return c.json({ message: 'Hello Hono!' }) }) export default app
c.json(), c.text(), c.html() for responsesres.send() like Express)npm install zod@4.3.5 @hono/zod-validator@0.7.6import { 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 }) })
// 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 }) })
c.req.param('name') returns single parameterc.req.param() returns all parameters as object// 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 }) })
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 }) })
// 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
await next() in middleware to continue the chainnext()) to prevent handler executionc.error AFTER next() for error handlingapp.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', }) )
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) } }) })
stream() - Binary files, video, audiostreamText() - AI chat responses, typewriter effectsstreamSSE() - Real-time notifications, live feedsimport { 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
hono/cloudflare-workers (not hono/ws)onOpen callback is NOT supported (Cloudflare limitation)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'], }))
secureHeaderscsrfbearerAuthbasicAuthipRestrictionimport { 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(), ))
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 }) })
Variables type for type-safe c.get()Bindings type for environment variables (Cloudflare Workers)c.set() in middleware, c.get() in handlersimport { 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' }) })
templates/context-extension.tsnpm install zod@4.3.5 @hono/zod-validator@0.7.6import { 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 }) })
c.req.valid() after validation (type-safe)json, query, param, header, form, cookiez.transform() to convert strings to numbers/datesapp.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 }) })
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.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 }) })
references/validation-libraries.mdnpm install typia @hono/typia-validator@0.1.2import { 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 }) })
npm install arktype @hono/arktype-validator@2.0.1import { 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 }) })
references/validation-libraries.md for detailed comparison// 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
const route = app.get(...) for RPC type inferencetypeof route or typeof app// 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 } }
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' }, })
// ❌ 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')
references/rpc-guide.mdimport { 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 }) })
onError insteadimport { 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!') })
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) })
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 inferenceawait 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 safetytypeof 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
// 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
tsc with .d.ts generation (for RPC client)tsc on main router, only type-check sub-routers (faster live-reload)z.omit(), z.extend(), z.pick() - These increase language server workload by 10xnotFound() 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 middlewareconst 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'> {} }
throw new Error() Prevention: Always use HTTPException for client errors (400-499)// ❌ Wrong throw new Error('Unauthorized') // ✅ Correct throw new HTTPException(401, { message: 'Unauthorized' })
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 typetype Variables = { user: { id: number; name: string } } const app = new Hono<{ Variables: Variables }>()
c.error after await next() Prevention: Check c.error in middlewareapp.use('*', async (c, next) => { await next() if (c.error) { console.error('Error:', c.error) } })
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 })
await next() runs handler, then bottom-to-topapp.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
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 parameterimport { 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
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 }) })
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.{ "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"] }
templates/ directory:/llmstxt/hono_dev_llms-full_txt{ "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" } }
references/top-errors.md firstawait next() is called in middlewareconst route = app.get(...) pattern