Loading...
Loading...
This skill provides comprehensive knowledge for building type-safe APIs with Hono, focusing on routing patterns, middleware composition, request validation, RPC client/server patterns, error handling, and context management. Use when: building APIs with Hono (any runtime), setting up request validation with Zod/Valibot/Typia/ArkType validators, creating type-safe RPC client/server communication, implementing custom middleware, handling errors with HTTPException, extending Hono context with custom variables, or encountering middleware type inference issues, validation hook confusion, or RPC performance problems. Keywords: hono, hono routing, hono middleware, hono rpc, hono validator, zod validator, valibot validator, type-safe api, hono context, hono error handling, HTTPException, c.req.valid, middleware composition, hono hooks, typed routes, hono client, middleware response not typed, hono validation failed, hono rpc type inference
npx skill4agent add jackspace/claudeskillz hono-routingnpm install hono@4.10.2import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.json({ message: 'Hello Hono!' })
})
export default appc.json()c.text()c.html()res.send()npm install zod@4.1.12 @hono/zod-validator@0.7.4import { 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 })
})import { Hono } from 'hono'
const app = new Hono()
// GET request
app.get('/posts', (c) => c.json({ posts: [] }))
// POST request
app.post('/posts', (c) => c.json({ created: true }))
// PUT request
app.put('/posts/:id', (c) => c.json({ updated: true }))
// DELETE request
app.delete('/posts/:id', (c) => c.json({ deleted: true }))
// Multiple methods
app.on(['GET', 'POST'], '/multi', (c) => c.text('GET or POST'))
// All methods
app.all('/catch-all', (c) => c.text('Any method'))// 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')c.req.param()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 })
})// Match any path after /api/
app.get('/api/*', (c) => {
const path = c.req.param('*')
return c.json({ catchAll: path })
})
// Named wildcard
app.get('/files/:filepath{.+}', (c) => {
const filepath = c.req.param('filepath')
return c.json({ file: filepath })
})// 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/postsimport { Hono } from 'hono'
const app = new Hono()
// Global middleware (runs for all routes)
app.use('*', async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next() // CRITICAL: Must call next()
console.log('Response sent')
})
// Route-specific middleware
app.use('/admin/*', async (c, next) => {
// Auth check
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
})
app.get('/admin/dashboard', (c) => {
return c.json({ message: 'Admin Dashboard' })
})await next()c.errornext()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',
})
)references/middleware-catalog.md// Multiple middleware in sequence
app.get(
'/protected',
authMiddleware,
rateLimitMiddleware,
(c) => {
return c.json({ data: 'Protected data' })
}
)
// Middleware factory pattern
const authMiddleware = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
// Set user in context
c.set('user', { id: 1, name: 'Alice' })
await next()
}
const rateLimitMiddleware = async (c, next) => {
// Rate limit logic
await next()
}// Timing middleware
const timing = async (c, next) => {
const start = Date.now()
await next()
const elapsed = Date.now() - start
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
}
// Request ID middleware
const requestId = async (c, next) => {
const id = crypto.randomUUID()
c.set('requestId', id)
await next()
c.res.headers.set('X-Request-ID', id)
}
// Error logging middleware
const errorLogger = async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
// Send to error tracking service
}
}
app.use('*', timing)
app.use('*', requestId)
app.use('*', errorLogger)c.set()c.errornext()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 })
})Variablesc.get()Bindingsc.set()c.get()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' })
})templates/context-extension.tsnpm install zod@4.1.12 @hono/zod-validator@0.7.4import { 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()jsonqueryparamheaderformcookiez.transform()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 })
}
)npm install valibot@1.1.0 @hono/valibot-validator@0.5.3import { 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// 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 appconst route = app.get(...)typeof routetypeof 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 } }// 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 })
})onErrorimport { 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
}
})app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Validation errors
app.post('/users', zValidator('json', schema), (c) => {
// zValidator automatically returns 400 on validation failure
const data = c.req.valid('json')
return c.json({ data })
})
// Authorization errors
app.use('/admin/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
await next()
})
// Not found errors
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await db.getUser(id)
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json({ user })
})
// Server errors
app.get('/data', async (c) => {
try {
const data = await fetchExternalAPI()
return c.json({ data })
} catch (error) {
// Let onError handle it
throw error
}
})
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error('Unexpected error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})await next()c.json()c.text()c.html()c.req.valid()export type AppType = typeof routeonErrorawait next()res.send()typeof app// ❌ Slow
export type AppType = typeof app
// ✅ Fast
const userRoutes = app.get(...).post(...)
export type UserRoutes = typeof userRoutesconst route = app.get(
'/data',
myMiddleware,
(c) => c.json({ data: 'value' })
)
export type AppType = typeof routethrow new Error()HTTPException// ❌ Wrong
throw new Error('Unauthorized')
// ✅ Correct
throw new HTTPException(401, { message: 'Unauthorized' })c.set()c.get()Variablestype Variables = {
user: { id: number; name: string }
}
const app = new Hono<{ Variables: Variables }>()c.errorawait next()c.errorapp.use('*', async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
}
})c.req.param()c.req.query()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()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{
"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.10.2"
},
"devDependencies": {
"typescript": "^5.9.0",
"tsx": "^4.19.0",
"@types/node": "^22.10.0"
}
}{
"dependencies": {
"hono": "^4.10.2",
"zod": "^4.1.12",
"@hono/zod-validator": "^0.7.4"
}
}{
"dependencies": {
"hono": "^4.10.2",
"valibot": "^1.1.0",
"@hono/valibot-validator": "^0.5.3"
}
}{
"dependencies": {
"hono": "^4.10.2",
"zod": "^4.1.12",
"valibot": "^1.1.0",
"@hono/zod-validator": "^0.7.4",
"@hono/valibot-validator": "^0.5.3",
"@hono/typia-validator": "^0.1.2",
"@hono/arktype-validator": "^2.0.1"
}
}{
"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//llmstxt/hono_dev_llms-full_txt{
"dependencies": {
"hono": "^4.10.2"
},
"optionalDependencies": {
"zod": "^4.1.12",
"valibot": "^1.1.0",
"@hono/zod-validator": "^0.7.4",
"@hono/valibot-validator": "^0.5.3",
"@hono/typia-validator": "^0.1.2",
"@hono/arktype-validator": "^2.0.1"
},
"devDependencies": {
"typescript": "^5.9.0"
}
}references/top-errors.mdawait next()const route = app.get(...)