Initial commit
This commit is contained in:
585
references/middleware-catalog.md
Normal file
585
references/middleware-catalog.md
Normal 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
542
references/rpc-guide.md
Normal 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
451
references/top-errors.md
Normal 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
|
||||
313
references/validation-libraries.md
Normal file
313
references/validation-libraries.md
Normal 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
|
||||
Reference in New Issue
Block a user