Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:59 +08:00
commit 6d0966aed4
18 changed files with 5925 additions and 0 deletions

View File

@@ -0,0 +1,585 @@
# Hono Built-in Middleware Catalog
Complete reference for all built-in Hono middleware with usage examples and configuration options.
**Last Updated**: 2025-10-22
**Hono Version**: 4.10.2+
---
## Installation
All built-in middleware are included in the `hono` package. No additional dependencies required.
```bash
npm install hono@4.10.2
```
---
## Request Logging
### logger()
Logs request method, path, status, and response time.
```typescript
import { logger } from 'hono/logger'
app.use('*', logger())
```
**Output**:
```
GET /api/users 200 - 15ms
POST /api/posts 201 - 42ms
```
**Custom logger**:
```typescript
import { logger } from 'hono/logger'
app.use(
'*',
logger((message, ...rest) => {
console.log(`[Custom] ${message}`, ...rest)
})
)
```
---
## CORS
### cors()
Enables Cross-Origin Resource Sharing.
```typescript
import { cors } from 'hono/cors'
// Simple usage
app.use('*', cors())
// Custom configuration
app.use(
'/api/*',
cors({
origin: ['https://example.com', 'https://app.example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['X-Request-ID', 'X-Response-Time'],
maxAge: 600,
credentials: true,
})
)
// Dynamic origin
app.use(
'*',
cors({
origin: (origin) => {
return origin.endsWith('.example.com') ? origin : 'https://example.com'
},
})
)
```
**Options**:
- `origin`: String, array, or function
- `allowMethods`: HTTP methods array
- `allowHeaders`: Headers array
- `exposeHeaders`: Headers to expose
- `maxAge`: Preflight cache duration (seconds)
- `credentials`: Allow credentials
---
## Pretty JSON
### prettyJSON()
Formats JSON responses with indentation (development only).
```typescript
import { prettyJSON } from 'hono/pretty-json'
if (process.env.NODE_ENV === 'development') {
app.use('*', prettyJSON())
}
```
**Options**:
```typescript
app.use(
'*',
prettyJSON({
space: 2, // Indentation spaces (default: 2)
})
)
```
---
## Compression
### compress()
Compresses responses using gzip or deflate.
```typescript
import { compress } from 'hono/compress'
app.use('*', compress())
// Custom options
app.use(
'*',
compress({
encoding: 'gzip', // 'gzip' | 'deflate'
})
)
```
**Behavior**:
- Automatically detects `Accept-Encoding` header
- Skips if `Content-Encoding` already set
- Skips if response is already compressed
---
## Caching
### cache()
Sets HTTP cache headers.
```typescript
import { cache } from 'hono/cache'
app.use(
'/static/*',
cache({
cacheName: 'my-app',
cacheControl: 'max-age=3600', // 1 hour
})
)
// Conditional caching
app.use(
'/api/public/*',
cache({
cacheName: 'api-cache',
cacheControl: 'public, max-age=300', // 5 minutes
wait: true, // Wait for cache to be ready
})
)
```
**Options**:
- `cacheName`: Cache name
- `cacheControl`: Cache-Control header value
- `wait`: Wait for cache to be ready
---
## ETag
### etag()
Generates and validates ETags for responses.
```typescript
import { etag } from 'hono/etag'
app.use('/api/*', etag())
// Custom options
app.use(
'/api/*',
etag({
weak: true, // Use weak ETags (W/"...")
})
)
```
**Behavior**:
- Automatically generates ETag from response body
- Returns 304 Not Modified if ETag matches
- Works with `If-None-Match` header
---
## Security Headers
### secureHeaders()
Sets security-related HTTP headers.
```typescript
import { secureHeaders } from 'hono/secure-headers'
app.use('*', secureHeaders())
// Custom configuration
app.use(
'*',
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
strictTransportSecurity: 'max-age=31536000; includeSubDomains',
xFrameOptions: 'DENY',
xContentTypeOptions: 'nosniff',
referrerPolicy: 'no-referrer',
})
)
```
**Headers set**:
- `Content-Security-Policy`
- `Strict-Transport-Security`
- `X-Frame-Options`
- `X-Content-Type-Options`
- `Referrer-Policy`
- `X-XSS-Protection` (deprecated but included)
---
## Server Timing
### timing()
Adds Server-Timing header with performance metrics.
```typescript
import { timing } from 'hono/timing'
app.use('*', timing())
// Custom timing
import { setMetric, startTime, endTime } from 'hono/timing'
app.use('*', timing())
app.get('/api/data', async (c) => {
startTime(c, 'db')
// Database query
endTime(c, 'db')
startTime(c, 'external')
// External API call
endTime(c, 'external')
setMetric(c, 'total', 150)
return c.json({ data: [] })
})
```
**Output header**:
```
Server-Timing: db;dur=45, external;dur=85, total;dur=150
```
---
## Bearer Auth
### bearerAuth()
Simple bearer token authentication.
```typescript
import { bearerAuth } from 'hono/bearer-auth'
app.use(
'/admin/*',
bearerAuth({
token: 'my-secret-token',
})
)
// Multiple tokens
app.use(
'/api/*',
bearerAuth({
token: ['token1', 'token2', 'token3'],
})
)
// Custom error
app.use(
'/api/*',
bearerAuth({
token: 'secret',
realm: 'My API',
onError: (c) => {
return c.json({ error: 'Unauthorized' }, 401)
},
})
)
```
---
## Basic Auth
### basicAuth()
HTTP Basic authentication.
```typescript
import { basicAuth } from 'hono/basic-auth'
app.use(
'/admin/*',
basicAuth({
username: 'admin',
password: 'secret',
})
)
// Custom validation
app.use(
'/api/*',
basicAuth({
verifyUser: async (username, password, c) => {
const user = await db.findUser(username)
if (!user) return false
return await bcrypt.compare(password, user.passwordHash)
},
})
)
```
---
## JWT
### jwt()
JSON Web Token authentication.
```typescript
import { jwt } from 'hono/jwt'
app.use(
'/api/*',
jwt({
secret: 'my-secret-key',
})
)
// Access JWT payload
app.get('/api/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})
// Custom algorithm
app.use(
'/api/*',
jwt({
secret: 'secret',
alg: 'HS256', // HS256 (default), HS384, HS512
})
)
```
---
## Request ID
### requestId()
Generates unique request IDs.
```typescript
import { requestId } from 'hono/request-id'
app.use('*', requestId())
app.get('/', (c) => {
const id = c.get('requestId')
return c.json({ requestId: id })
})
// Custom generator
app.use(
'*',
requestId({
generator: () => `req-${Date.now()}-${Math.random()}`,
})
)
```
---
## Combine
### combine()
Combines multiple middleware into one.
```typescript
import { combine } from 'hono/combine'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { compress } from 'hono/compress'
app.use('*', combine(
logger(),
cors(),
compress()
))
```
---
## Trailing Slash
### trimTrailingSlash()
Removes trailing slashes from URLs.
```typescript
import { trimTrailingSlash } from 'hono/trailing-slash'
app.use('*', trimTrailingSlash())
// /api/users/ → /api/users
```
---
## Serve Static
### serveStatic()
Serves static files (runtime-specific).
**Cloudflare Workers**:
```typescript
import { serveStatic } from 'hono/cloudflare-workers'
app.use('/static/*', serveStatic({ root: './public' }))
```
**Deno**:
```typescript
import { serveStatic } from 'hono/deno'
app.use('/static/*', serveStatic({ root: './public' }))
```
**Bun**:
```typescript
import { serveStatic } from 'hono/bun'
app.use('/static/*', serveStatic({ root: './public' }))
```
---
## Body Limit
### bodyLimit()
Limits request body size.
```typescript
import { bodyLimit } from 'hono/body-limit'
app.use(
'/api/*',
bodyLimit({
maxSize: 1024 * 1024, // 1 MB
onError: (c) => {
return c.json({ error: 'Request body too large' }, 413)
},
})
)
```
---
## Timeout
### timeout()
Sets timeout for requests.
```typescript
import { timeout } from 'hono/timeout'
app.use(
'/api/*',
timeout(5000) // 5 seconds
)
// Custom error
app.use(
'/api/*',
timeout(
3000,
(c) => c.json({ error: 'Request timeout' }, 504)
)
)
```
---
## IP Restriction
### ipRestriction()
Restricts access by IP address.
```typescript
import { ipRestriction } from 'hono/ip-restriction'
app.use(
'/admin/*',
ipRestriction(
{
allowList: ['192.168.1.0/24'],
denyList: ['10.0.0.0/8'],
},
(c) => c.json({ error: 'Forbidden' }, 403)
)
)
```
---
## Summary
| Middleware | Purpose | Common Use Case |
|------------|---------|-----------------|
| `logger()` | Request logging | Development, debugging |
| `cors()` | CORS handling | API routes |
| `prettyJSON()` | JSON formatting | Development |
| `compress()` | Response compression | All routes |
| `cache()` | HTTP caching | Static assets |
| `etag()` | ETag generation | API responses |
| `secureHeaders()` | Security headers | All routes |
| `timing()` | Performance metrics | Production monitoring |
| `bearerAuth()` | Bearer token auth | API authentication |
| `basicAuth()` | Basic auth | Admin panels |
| `jwt()` | JWT authentication | API authentication |
| `requestId()` | Request IDs | Logging, tracing |
| `combine()` | Combine middleware | Clean code |
| `trimTrailingSlash()` | URL normalization | All routes |
| `serveStatic()` | Static files | Assets |
| `bodyLimit()` | Body size limit | API routes |
| `timeout()` | Request timeout | Long-running operations |
| `ipRestriction()` | IP filtering | Admin panels |
---
**Official Documentation**: https://hono.dev/docs/guides/middleware

542
references/rpc-guide.md Normal file
View File

@@ -0,0 +1,542 @@
# Hono RPC Pattern Deep Dive
Complete guide to type-safe client/server communication using Hono's RPC feature.
**Last Updated**: 2025-10-22
---
## What is Hono RPC?
Hono RPC allows you to create **fully type-safe client/server communication** without manually defining API types. The client automatically infers types directly from server routes.
**Key Benefits**:
- ✅ Full type inference (request + response)
- ✅ No manual type definitions
- ✅ Compile-time error checking
- ✅ Auto-complete in IDE
- ✅ Refactoring safety
---
## Basic Setup
### Server
```typescript
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
})
// CRITICAL: Use const route = app.get(...) pattern
const route = app.post(
'/users',
zValidator('json', userSchema),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, user: { id: '1', ...data } }, 201)
}
)
// Export type for RPC client
export type AppType = typeof route
export default app
```
### Client
```typescript
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// Type-safe API call
const res = await client.users.$post({
json: {
name: 'Alice',
email: 'alice@example.com',
},
})
const data = await res.json()
// Type: { success: boolean, user: { id: string, name: string, email: string } }
```
---
## Export Patterns
### Pattern 1: Export Single Route
```typescript
const route = app.get('/users', handler)
export type AppType = typeof route
```
**When to use**: Single route, simple API
### Pattern 2: Export Multiple Routes
```typescript
const getUsers = app.get('/users', handler)
const createUser = app.post('/users', handler)
const getUser = app.get('/users/:id', handler)
export type AppType = typeof getUsers | typeof createUser | typeof getUser
```
**When to use**: Multiple routes, moderate complexity
### Pattern 3: Export Sub-apps
```typescript
const usersApp = new Hono()
usersApp.get('/', handler)
usersApp.post('/', handler)
export type UsersType = typeof usersApp
```
**When to use**: Organized route groups, large APIs
### Pattern 4: Export Entire App
```typescript
const app = new Hono()
// ... many routes ...
export type AppType = typeof app
```
**When to use**: Small apps only (performance issue with large apps)
---
## Performance Optimization
### Problem: Slow Type Inference
With many routes, exporting `typeof app` causes slow IDE performance due to complex type instantiation.
### Solution: Export Specific Route Groups
```typescript
// ❌ Slow: 100+ routes
export type AppType = typeof app
// ✅ Fast: Export by domain
const userRoutes = app.basePath('/users').get('/', ...).post('/', ...)
const postRoutes = app.basePath('/posts').get('/', ...).post('/', ...)
export type UserRoutes = typeof userRoutes
export type PostRoutes = typeof postRoutes
// Client uses specific routes
const userClient = hc<UserRoutes>('http://localhost:8787/users')
const postClient = hc<PostRoutes>('http://localhost:8787/posts')
```
---
## Client Usage Patterns
### Basic GET Request
```typescript
const res = await client.users.$get()
const data = await res.json()
```
### POST with JSON Body
```typescript
const res = await client.users.$post({
json: {
name: 'Alice',
email: 'alice@example.com',
},
})
const data = await res.json()
```
### Route Parameters
```typescript
const res = await client.users[':id'].$get({
param: { id: '123' },
})
const data = await res.json()
```
### Query Parameters
```typescript
const res = await client.search.$get({
query: {
q: 'hello',
page: '2',
},
})
const data = await res.json()
```
### Custom Headers
```typescript
const res = await client.users.$get({}, {
headers: {
Authorization: 'Bearer token',
},
})
```
### Fetch Options
```typescript
const res = await client.users.$get({}, {
signal: AbortSignal.timeout(5000), // 5 second timeout
cache: 'no-cache',
})
```
---
## Error Handling
### Basic Error Handling
```typescript
const res = await client.users.$post({
json: { name: 'Alice', email: 'alice@example.com' },
})
if (!res.ok) {
console.error('Request failed:', res.status)
return
}
const data = await res.json()
```
### Typed Error Responses
```typescript
const res = await client.users.$post({
json: { name: '', email: 'invalid' },
})
if (res.status === 400) {
const error = await res.json() // Typed as error response
console.error('Validation error:', error)
return
}
if (res.status === 500) {
const error = await res.json()
console.error('Server error:', error)
return
}
const data = await res.json() // Typed as success response
```
### Try-Catch
```typescript
try {
const res = await client.users.$get()
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
const data = await res.json()
console.log('Users:', data)
} catch (error) {
console.error('Network error:', error)
}
```
---
## Authentication
### Bearer Token
```typescript
const client = hc<AppType>('http://localhost:8787', {
headers: {
Authorization: 'Bearer your-token-here',
},
})
const res = await client.protected.$get()
```
### Dynamic Headers
```typescript
async function makeAuthenticatedRequest() {
const token = await getAuthToken()
const res = await client.protected.$get({}, {
headers: {
Authorization: `Bearer ${token}`,
},
})
return res.json()
}
```
---
## React Integration
### Basic Hook
```typescript
import { useState, useEffect } from 'react'
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
function useUsers() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
async function fetchUsers() {
setLoading(true)
setError(null)
try {
const res = await client.users.$get()
if (!res.ok) {
throw new Error('Failed to fetch users')
}
const data = await res.json()
setUsers(data.users)
} catch (err) {
setError(err as Error)
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
return { users, loading, error }
}
```
### With TanStack Query
```typescript
import { useQuery } from '@tanstack/react-query'
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await client.users.$get()
if (!res.ok) {
throw new Error('Failed to fetch users')
}
return res.json()
},
})
}
function UsersComponent() {
const { data, isLoading, error } = useUsers()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data?.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
```
---
## Advanced Patterns
### Middleware Responses
Server:
```typescript
const authMiddleware = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
}
const route = app.get('/protected', authMiddleware, (c) => {
return c.json({ data: 'Protected data' })
})
export type ProtectedType = typeof route
```
Client:
```typescript
const res = await client.protected.$get()
// Response type includes both middleware (401) and handler (200) responses
if (res.status === 401) {
const error = await res.json() // Type: { error: string }
console.error('Unauthorized:', error)
return
}
const data = await res.json() // Type: { data: string }
```
### Multiple Sub-apps
Server:
```typescript
const usersApp = new Hono()
usersApp.get('/', handler)
usersApp.post('/', handler)
const postsApp = new Hono()
postsApp.get('/', handler)
postsApp.post('/', handler)
const app = new Hono()
app.route('/users', usersApp)
app.route('/posts', postsApp)
export type UsersType = typeof usersApp
export type PostsType = typeof postsApp
```
Client:
```typescript
const userClient = hc<UsersType>('http://localhost:8787/users')
const postClient = hc<PostsType>('http://localhost:8787/posts')
const users = await userClient.index.$get()
const posts = await postClient.index.$get()
```
---
## TypeScript Tips
### Type Inference
```typescript
// Infer request type
type UserRequest = Parameters<typeof client.users.$post>[0]['json']
// Type: { name: string, email: string }
// Infer response type
type UserResponse = Awaited<ReturnType<typeof client.users.$post>>
type UserData = Awaited<ReturnType<UserResponse['json']>>
// Type: { success: boolean, user: { id: string, name: string, email: string } }
```
### Generic Client Functions
```typescript
async function fetchFromAPI<T extends typeof client[keyof typeof client]>(
endpoint: T,
options?: Parameters<T['$get']>[0]
) {
const res = await endpoint.$get(options)
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
return res.json()
}
// Usage
const users = await fetchFromAPI(client.users)
```
---
## Performance Best Practices
1. **Export specific routes** for large APIs
2. **Use route groups** for better organization
3. **Batch requests** when possible
4. **Cache client instance** (don't recreate on every request)
5. **Use AbortController** for request cancellation
---
## Common Pitfalls
### ❌ Don't: Anonymous Routes
```typescript
app.get('/users', (c) => c.json({ users: [] }))
export type AppType = typeof app // Won't infer route properly
```
### ✅ Do: Named Routes
```typescript
const route = app.get('/users', (c) => c.json({ users: [] }))
export type AppType = typeof route
```
### ❌ Don't: Forget Type Import
```typescript
import { AppType } from './server' // Wrong: runtime import
```
### ✅ Do: Type-Only Import
```typescript
import type { AppType } from './server' // Correct: type-only import
```
---
## Official Documentation
- **Hono RPC Guide**: https://hono.dev/docs/guides/rpc
- **hc Client API**: https://hono.dev/docs/helpers/hc

451
references/top-errors.md Normal file
View File

@@ -0,0 +1,451 @@
# Common Hono Errors and Solutions
Complete troubleshooting guide for Hono routing and middleware errors.
**Last Updated**: 2025-10-22
---
## Error #1: Middleware Response Not Typed in RPC
**Error Message**: Client doesn't infer middleware response types
**Cause**: RPC mode doesn't automatically infer middleware responses by default
**Source**: [honojs/hono#2719](https://github.com/honojs/hono/issues/2719)
**Solution**: Export specific route types that include middleware
```typescript
// ❌ Wrong: Client doesn't see middleware response
const route = app.get('/data', authMiddleware, handler)
export type AppType = typeof app
// ✅ Correct: Export route directly
const route = app.get('/data', authMiddleware, handler)
export type AppType = typeof route
```
---
## Error #2: RPC Type Inference Slow
**Error Message**: IDE becomes slow or unresponsive with many routes
**Cause**: Complex type instantiation from `typeof app` with large number of routes
**Source**: [hono.dev/docs/guides/rpc](https://hono.dev/docs/guides/rpc)
**Solution**: Export specific route groups instead of entire app
```typescript
// ❌ 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
```
---
## Error #3: Middleware Chain Broken
**Error Message**: Handler not executed, middleware returns early
**Cause**: Forgot to call `await next()` in middleware
**Source**: Official docs
**Solution**: Always call `await next()` unless intentionally short-circuiting
```typescript
// ❌ Wrong: Forgot await next()
app.use('*', async (c, next) => {
console.log('Before')
// Missing: await next()
console.log('After')
})
// ✅ Correct: Call await next()
app.use('*', async (c, next) => {
console.log('Before')
await next()
console.log('After')
})
```
---
## Error #4: Validation Error Not Handled
**Error Message**: Validation fails silently or returns wrong status code
**Cause**: No custom error handler for validation failures
**Source**: Best practices
**Solution**: Use custom validation hooks
```typescript
// ❌ Wrong: Default 400 response with no details
app.post('/users', zValidator('json', schema), handler)
// ✅ Correct: Custom error handler
app.post(
'/users',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.json({ error: 'Validation failed', issues: result.error.issues }, 400)
}
}),
handler
)
```
---
## Error #5: Context Type Safety Lost
**Error Message**: `c.get()` returns `any` type
**Cause**: Not defining `Variables` type in Hono generic
**Source**: Official docs
**Solution**: Define Variables type
```typescript
// ❌ Wrong: No Variables type
const app = new Hono()
app.use('*', (c, next) => {
c.set('user', { id: 1 }) // No type checking
await next()
})
// ✅ Correct: Define Variables type
type Variables = {
user: { id: number; name: string }
}
const app = new Hono<{ Variables: Variables }>()
app.use('*', (c, next) => {
c.set('user', { id: 1, name: 'Alice' }) // Type-safe!
await next()
})
```
---
## Error #6: Route Parameter Type Error
**Error Message**: `c.req.param()` returns string but number expected
**Cause**: Route parameters are always strings
**Source**: Official docs
**Solution**: Use validation to transform to correct type
```typescript
// ❌ Wrong: Assuming number type
app.get('/users/:id', (c) => {
const id = c.req.param('id') // Type: string
const user = await db.findUser(id) // Error: expects number
return c.json({ user })
})
// ✅ Correct: Validate and transform
const idSchema = z.object({
id: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
})
app.get('/users/:id', zValidator('param', idSchema), async (c) => {
const { id } = c.req.valid('param') // Type: number
const user = await db.findUser(id)
return c.json({ user })
})
```
---
## Error #7: Missing Error Check After Middleware
**Error Message**: Errors in handlers not caught
**Cause**: Not checking `c.error` after `await next()`
**Source**: Official docs
**Solution**: Check `c.error` in middleware
```typescript
// ❌ Wrong: No error checking
app.use('*', async (c, next) => {
await next()
// Missing error check
})
// ✅ Correct: Check c.error
app.use('*', async (c, next) => {
await next()
if (c.error) {
console.error('Error:', c.error)
// Send to error tracking service
}
})
```
---
## Error #8: HTTPException Misuse
**Error Message**: Errors not handled correctly
**Cause**: Throwing plain Error instead of HTTPException
**Source**: Official docs
**Solution**: Use HTTPException for client errors
```typescript
// ❌ Wrong: Plain Error
app.get('/users/:id', (c) => {
if (!id) {
throw new Error('ID is required') // No status code
}
})
// ✅ Correct: HTTPException
import { HTTPException } from 'hono/http-exception'
app.get('/users/:id', (c) => {
if (!id) {
throw new HTTPException(400, { message: 'ID is required' })
}
})
```
---
## Error #9: Query Parameter Not Validated
**Error Message**: Invalid query parameters cause errors
**Cause**: Accessing `c.req.query()` without validation
**Source**: Best practices
**Solution**: Validate query parameters
```typescript
// ❌ Wrong: No validation
app.get('/search', (c) => {
const page = parseInt(c.req.query('page') || '1', 10) // May be NaN
const limit = parseInt(c.req.query('limit') || '10', 10)
// ...
})
// ✅ Correct: Validate query params
const querySchema = z.object({
page: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1)),
limit: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(100)),
})
app.get('/search', zValidator('query', querySchema), (c) => {
const { page, limit } = c.req.valid('query') // Type-safe!
// ...
})
```
---
## Error #10: Incorrect Middleware Order
**Error Message**: Middleware executing in wrong order
**Cause**: Misunderstanding middleware execution flow
**Source**: Official docs
**Solution**: Remember middleware runs top-to-bottom before handler, bottom-to-top after
```typescript
// Middleware execution order:
app.use('*', async (c, next) => {
console.log('1: Before') // Runs 1st
await next()
console.log('4: After') // Runs 4th
})
app.use('*', async (c, next) => {
console.log('2: Before') // Runs 2nd
await next()
console.log('3: After') // Runs 3rd
})
app.get('/', (c) => {
console.log('Handler') // Runs in between
return c.json({})
})
// Output: 1, 2, Handler, 3, 4
```
---
## Error #11: JSON Parsing Error
**Error Message**: `SyntaxError: Unexpected token in JSON`
**Cause**: Request body is not valid JSON
**Source**: Common issue
**Solution**: Add validation and error handling
```typescript
app.post('/data', async (c) => {
try {
const body = await c.req.json()
return c.json({ success: true, body })
} catch (error) {
return c.json({ error: 'Invalid JSON' }, 400)
}
})
// Or use validator (handles automatically)
app.post('/data', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
```
---
## Error #12: CORS Preflight Fails
**Error Message**: CORS preflight request fails
**Cause**: Missing CORS middleware or incorrect configuration
**Source**: Common issue
**Solution**: Configure CORS middleware correctly
```typescript
import { cors } from 'hono/cors'
app.use(
'/api/*',
cors({
origin: ['https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
})
)
```
---
## Error #13: Route Not Found
**Error Message**: 404 Not Found for existing route
**Cause**: Route pattern doesn't match request path
**Source**: Common issue
**Solution**: Check route pattern and parameter syntax
```typescript
// ❌ Wrong: Missing colon for parameter
app.get('/users/id', handler) // Matches "/users/id" literally
// ✅ Correct: Parameter with colon
app.get('/users/:id', handler) // Matches "/users/123"
```
---
## Error #14: Response Already Sent
**Error Message**: Cannot set headers after response sent
**Cause**: Trying to modify response after calling `c.json()` or `c.text()`
**Source**: Common issue
**Solution**: Return response immediately, don't modify after
```typescript
// ❌ Wrong: Trying to modify after response
app.get('/data', (c) => {
const response = c.json({ data: 'value' })
c.res.headers.set('X-Custom', 'value') // Error!
return response
})
// ✅ Correct: Set headers before response
app.get('/data', (c) => {
c.res.headers.set('X-Custom', 'value')
return c.json({ data: 'value' })
})
```
---
## Error #15: Type Inference Not Working
**Error Message**: TypeScript not inferring types from validator
**Cause**: Not using `c.req.valid()` after validation
**Source**: Official docs
**Solution**: Always use `c.req.valid()` for type-safe access
```typescript
// ❌ Wrong: No type inference
app.post('/users', zValidator('json', schema), async (c) => {
const body = await c.req.json() // Type: any
return c.json({ body })
})
// ✅ Correct: Type-safe
app.post('/users', zValidator('json', schema), (c) => {
const data = c.req.valid('json') // Type: inferred from schema
return c.json({ data })
})
```
---
## Quick Reference
| Error | Cause | Solution |
|-------|-------|----------|
| Middleware response not typed | RPC doesn't infer middleware | Export route, not app |
| Slow RPC type inference | Too many routes | Export specific route groups |
| Middleware chain broken | Missing `await next()` | Always call `await next()` |
| Validation error unhandled | No custom hook | Use custom validation hook |
| Context type safety lost | No Variables type | Define Variables type |
| Route param type error | Params are strings | Use validation to transform |
| Missing error check | Not checking `c.error` | Check `c.error` after `next()` |
| HTTPException misuse | Using plain Error | Use HTTPException |
| Query param not validated | Direct access | Use query validator |
| Incorrect middleware order | Misunderstanding flow | Review execution order |
| JSON parsing error | Invalid JSON | Add error handling |
| CORS preflight fails | Missing CORS config | Configure CORS middleware |
| Route not found | Wrong pattern | Check route syntax |
| Response already sent | Modifying after send | Set headers before response |
| Type inference not working | Not using `c.req.valid()` | Use `c.req.valid()` |
---
**Official Documentation**: https://hono.dev/docs

View File

@@ -0,0 +1,313 @@
# Validation Libraries Comparison
Comprehensive comparison of validation libraries for Hono: Zod, Valibot, Typia, and ArkType.
**Last Updated**: 2025-10-22
---
## Quick Comparison
| Feature | Zod | Valibot | Typia | ArkType |
|---------|-----|---------|-------|---------|
| **Bundle Size** | ~57KB | ~1-5KB | ~0KB | ~15KB |
| **Performance** | Good | Excellent | Best | Excellent |
| **Type Safety** | Excellent | Excellent | Best | Excellent |
| **Ecosystem** | Largest | Growing | Small | Growing |
| **Learning Curve** | Easy | Easy | Medium | Easy |
| **Compilation** | Runtime | Runtime | AOT | Runtime |
| **Tree Shaking** | Limited | Excellent | N/A | Good |
---
## Zod
**Install**: `npm install zod @hono/zod-validator`
**Pros**:
- ✅ Most popular (11M+ weekly downloads)
- ✅ Extensive ecosystem and community
- ✅ Excellent TypeScript support
- ✅ Rich feature set (transforms, refinements, etc.)
- ✅ Great documentation
**Cons**:
- ❌ Larger bundle size (~57KB)
- ❌ Slower performance vs alternatives
- ❌ Limited tree-shaking
**Best for**:
- Production applications with complex validation needs
- Projects prioritizing ecosystem and community support
- Teams familiar with Zod
**Example**:
```typescript
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional(),
})
app.post('/users', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
```
---
## Valibot
**Install**: `npm install valibot @hono/valibot-validator`
**Pros**:
- ✅ Tiny bundle size (1-5KB with tree-shaking)
- ✅ Excellent performance
- ✅ Modular design (import only what you need)
- ✅ Similar API to Zod
- ✅ Great TypeScript support
**Cons**:
- ❌ Smaller ecosystem vs Zod
- ❌ Newer library (less battle-tested)
- ❌ Fewer integrations
**Best for**:
- Applications prioritizing bundle size
- Performance-critical applications
- Projects that want Zod-like API with better performance
**Example**:
```typescript
import { vValidator } from '@hono/valibot-validator'
import * as v from 'valibot'
const schema = v.object({
name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(18))),
})
app.post('/users', vValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
```
---
## Typia
**Install**: `npm install typia @hono/typia-validator`
**Pros**:
-**Fastest** validation (AOT compilation)
- ✅ Zero runtime overhead
- ✅ No bundle size impact
- ✅ Uses TypeScript types directly
- ✅ Compile-time validation
**Cons**:
- ❌ Requires build step (TypeScript transformer)
- ❌ More complex setup
- ❌ Smaller community
- ❌ Limited to TypeScript
**Best for**:
- Maximum performance requirements
- Applications with strict bundle size constraints
- Projects already using TypeScript transformers
**Example**:
```typescript
import { typiaValidator } from '@hono/typia-validator'
import typia from 'typia'
interface User {
name: string
email: 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 })
})
```
---
## ArkType
**Install**: `npm install arktype @hono/arktype-validator`
**Pros**:
- ✅ Excellent performance (1.5x faster than Zod)
- ✅ Intuitive string-based syntax
- ✅ Great error messages
- ✅ TypeScript-first
- ✅ Small bundle size (~15KB)
**Cons**:
- ❌ Newer library
- ❌ Smaller ecosystem
- ❌ Different syntax (learning curve)
**Best for**:
- Developers who prefer string-based schemas
- Performance-conscious projects
- Projects that value developer experience
**Example**:
```typescript
import { arktypeValidator } from '@hono/arktype-validator'
import { type } from 'arktype'
const schema = type({
name: 'string',
email: 'email',
'age?': 'number>=18',
})
app.post('/users', arktypeValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
```
---
## Performance Benchmarks
Based on community benchmarks (approximate):
```
Typia: ~10,000,000 validations/sec (Fastest - AOT)
Valibot: ~5,000,000 validations/sec
ArkType: ~3,500,000 validations/sec
Zod: ~2,300,000 validations/sec
```
**Note**: Actual performance varies by schema complexity and runtime.
---
## Bundle Size Comparison
```
Typia: 0 KB (AOT compilation)
Valibot: 1-5 KB (with tree-shaking)
ArkType: ~15 KB
Zod: ~57 KB
```
---
## Feature Comparison
### Transformations
| Library | Support | Example |
|---------|---------|---------|
| Zod | ✅ | `z.string().transform(Number)` |
| Valibot | ✅ | `v.pipe(v.string(), v.transform(Number))` |
| Typia | ✅ | Built into types |
| ArkType | ✅ | Type inference |
### Refinements
| Library | Support | Example |
|---------|---------|---------|
| Zod | ✅ | `z.string().refine((val) => val.length > 0)` |
| Valibot | ✅ | `v.pipe(v.string(), v.check((val) => val.length > 0))` |
| Typia | ✅ | Custom validators |
| ArkType | ✅ | Narrow types |
### Default Values
| Library | Support | Example |
|---------|---------|---------|
| Zod | ✅ | `z.string().default('default')` |
| Valibot | ✅ | `v.optional(v.string(), 'default')` |
| Typia | ⚠️ | Limited |
| ArkType | ✅ | Type defaults |
---
## Recommendations
### Choose **Zod** if:
- You want the largest ecosystem and community
- You need extensive documentation and examples
- Bundle size is not a primary concern
- You want battle-tested reliability
### Choose **Valibot** if:
- Bundle size is critical
- You want Zod-like API with better performance
- You're building a modern application with tree-shaking
- You want modular imports
### Choose **Typia** if:
- Performance is absolutely critical
- You can afford a more complex build setup
- Zero runtime overhead is required
- You're already using TypeScript transformers
### Choose **ArkType** if:
- You prefer string-based schema syntax
- You want excellent error messages
- Performance is important but not critical
- You value developer experience
---
## Migration Guide
### Zod → Valibot
```typescript
// Zod
const schema = z.object({
name: z.string().min(1),
age: z.number().optional(),
})
// Valibot
const schema = v.object({
name: v.pipe(v.string(), v.minLength(1)),
age: v.optional(v.number()),
})
```
### Zod → ArkType
```typescript
// Zod
const schema = z.object({
name: z.string().min(1),
age: z.number().optional(),
})
// ArkType
const schema = type({
name: 'string>0',
'age?': 'number',
})
```
---
## Official Documentation
- **Zod**: https://zod.dev
- **Valibot**: https://valibot.dev
- **Typia**: https://typia.io
- **ArkType**: https://arktype.io
- **Hono Validators**: https://hono.dev/docs/guides/validation