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,12 @@
{
"name": "hono-routing",
"description": "Build type-safe APIs with Hono - fast, lightweight routing for Cloudflare Workers, Deno, Bun, and Node.js. Set up routing patterns, middleware composition, request validation (Zod/Valibot/Typia/ArkType), RPC client/server with full type inference, and error handling with HTTPException. Use when: building APIs with Hono, setting up request validation with schema libraries, creating type-safe RPC client/server communication, implementing custom middleware chains, handling errors with HTTPException",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# hono-routing
Build type-safe APIs with Hono - fast, lightweight routing for Cloudflare Workers, Deno, Bun, and Node.js. Set up routing patterns, middleware composition, request validation (Zod/Valibot/Typia/ArkType), RPC client/server with full type inference, and error handling with HTTPException. Use when: building APIs with Hono, setting up request validation with schema libraries, creating type-safe RPC client/server communication, implementing custom middleware chains, handling errors with HTTPException

1050
SKILL.md Normal file

File diff suppressed because it is too large Load Diff

101
plugin.lock.json Normal file
View File

@@ -0,0 +1,101 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jezweb/claude-skills:skills/hono-routing",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "2170fdd7ad5b1c855babb95251ab4b7ee27ececb",
"treeHash": "89071a7dd858377c2ba059343a9b787a536bc0c32e2bc002ecd315ee993b7c36",
"generatedAt": "2025-11-28T10:19:00.059537Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "hono-routing",
"description": "Build type-safe APIs with Hono - fast, lightweight routing for Cloudflare Workers, Deno, Bun, and Node.js. Set up routing patterns, middleware composition, request validation (Zod/Valibot/Typia/ArkType), RPC client/server with full type inference, and error handling with HTTPException. Use when: building APIs with Hono, setting up request validation with schema libraries, creating type-safe RPC client/server communication, implementing custom middleware chains, handling errors with HTTPException",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "34f1b71026990c16a80250f429bf499b4d650683f3d859803ea3513e68cead1b"
},
{
"path": "SKILL.md",
"sha256": "4ac1a1e4fd7c67d0c555899d0eea202e6306f2fc66a2153329725a994a003e0f"
},
{
"path": "references/validation-libraries.md",
"sha256": "2e2e7e43cb5bd424d0634d9825af221c5df1df657cfd0e8632447c2c8cf85b22"
},
{
"path": "references/rpc-guide.md",
"sha256": "f9665e9b3355ba858a9dc38a935ae6e042aa182d26738e75166178ce02e3d8a1"
},
{
"path": "references/top-errors.md",
"sha256": "3a2d8905b3e3bd45f09306a0b856bb578736da3015c81ef2c835f9584935bf65"
},
{
"path": "references/middleware-catalog.md",
"sha256": "4f9f6f42c3b8d01c496199eb0cc081f45ff4baed87f20430aef880001784fde8"
},
{
"path": "scripts/check-versions.sh",
"sha256": "c24f4d0dd7748d58130e8518adc35929b5a8a41638cf0f23785b3f2e424f3e87"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "64bdb0f61d22d2113135c20a04c51b97534c258947a16fd18639fbe1df857173"
},
{
"path": "templates/rpc-client.ts",
"sha256": "cc9b54688da6366163b5722dc77328fda7ae659c02e68945aebd76f30c3a8d40"
},
{
"path": "templates/validation-valibot.ts",
"sha256": "acdfba7cd687b19a53486de7b605d6055ea7aae9f056e16593e9b5901cbc5c24"
},
{
"path": "templates/middleware-composition.ts",
"sha256": "ad65bec12642e689bff58d650063aa6790a0854de2027391860798df44a507db"
},
{
"path": "templates/routing-patterns.ts",
"sha256": "6a1e24543449f39f22f7c8add643bda78bb817ea4128d3bcb35e1f0b147ecb5c"
},
{
"path": "templates/validation-zod.ts",
"sha256": "efefea84dba234b533ac89f205d3c1afd8f448e4b18a140e45a5f4480cd5f3c4"
},
{
"path": "templates/error-handling.ts",
"sha256": "9a6875b6a2ee1d6a1e482ec96c017b6e8c795b9e3538248f29312b2c23d8df0a"
},
{
"path": "templates/package.json",
"sha256": "f1449e152e9e310953a810685457871e6e306865aa6cb9dbc892901250ff0e32"
},
{
"path": "templates/rpc-pattern.ts",
"sha256": "f0b09fcba8a6c55c8848a09430007d7c947a0194a37c37303bc0335d19c2a505"
},
{
"path": "templates/context-extension.ts",
"sha256": "42dbc6f13889e9900d209c421cd8e23c8e6891033d3c55be22bdc2b19b4653da"
}
],
"dirSha256": "89071a7dd858377c2ba059343a9b787a536bc0c32e2bc002ecd315ee993b7c36"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

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

74
scripts/check-versions.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Hono Skill - Package Version Checker
# Verifies that all package versions are current
echo "🔍 Checking Hono skill package versions..."
echo ""
# Color codes
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Counter for outdated packages
OUTDATED=0
# Function to check package version
check_package() {
local package=$1
local current_version=$2
echo -n "Checking $package... "
# Get latest version from npm
latest=$(npm view "$package" version 2>/dev/null)
if [ $? -ne 0 ]; then
echo -e "${RED}ERROR${NC} (package not found)"
return 1
fi
if [ "$current_version" == "$latest" ]; then
echo -e "${GREEN}${NC} $current_version (latest)"
else
echo -e "${YELLOW}${NC} $current_version$latest (update available)"
((OUTDATED++))
fi
}
echo "Core Dependencies:"
echo "─────────────────"
check_package "hono" "4.10.2"
echo ""
echo "Validation Libraries:"
echo "────────────────────"
check_package "zod" "4.1.12"
check_package "valibot" "1.1.0"
echo ""
echo "Hono Validators:"
echo "───────────────"
check_package "@hono/zod-validator" "0.7.4"
check_package "@hono/valibot-validator" "0.5.3"
check_package "@hono/typia-validator" "0.1.2"
check_package "@hono/arktype-validator" "2.0.1"
echo ""
echo "Summary:"
echo "────────"
if [ $OUTDATED -eq 0 ]; then
echo -e "${GREEN}${NC} All packages are up to date!"
else
echo -e "${YELLOW}${NC} $OUTDATED package(s) have updates available"
echo ""
echo "To update, run:"
echo " npm install hono@latest"
echo " npm install zod@latest valibot@latest"
echo " npm install @hono/zod-validator@latest @hono/valibot-validator@latest"
fi
echo ""
echo "Last checked: $(date)"

View File

@@ -0,0 +1,422 @@
/**
* Hono Context Extension
*
* Type-safe context extension using c.set() and c.get() with custom Variables.
*/
import { Hono } from 'hono'
import type { Context, Next } from 'hono'
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
// Define environment bindings (for Cloudflare Workers, etc.)
type Bindings = {
DATABASE_URL: string
API_KEY: string
ENVIRONMENT: 'development' | 'staging' | 'production'
}
// Define context variables (c.set/c.get)
type Variables = {
user: {
id: string
email: string
name: string
role: 'admin' | 'user'
}
requestId: string
startTime: number
logger: {
info: (message: string, meta?: any) => void
warn: (message: string, meta?: any) => void
error: (message: string, meta?: any) => void
}
db: {
query: <T>(sql: string, params?: any[]) => Promise<T[]>
execute: (sql: string, params?: any[]) => Promise<void>
}
cache: {
get: (key: string) => Promise<string | null>
set: (key: string, value: string, ttl?: number) => Promise<void>
delete: (key: string) => Promise<void>
}
}
// Create typed app
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// ============================================================================
// REQUEST ID MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const requestId = crypto.randomUUID()
c.set('requestId', requestId)
await next()
c.res.headers.set('X-Request-ID', requestId)
})
// ============================================================================
// PERFORMANCE TIMING MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const startTime = Date.now()
c.set('startTime', startTime)
await next()
const elapsed = Date.now() - startTime
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
const logger = c.get('logger')
logger.info(`Request completed in ${elapsed}ms`, {
path: c.req.path,
method: c.req.method,
})
})
// ============================================================================
// LOGGER MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const requestId = c.get('requestId')
const logger = {
info: (message: string, meta?: any) => {
console.log(
JSON.stringify({
level: 'info',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
warn: (message: string, meta?: any) => {
console.warn(
JSON.stringify({
level: 'warn',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
error: (message: string, meta?: any) => {
console.error(
JSON.stringify({
level: 'error',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
}
c.set('logger', logger)
await next()
})
// ============================================================================
// DATABASE MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
// Simulated database connection
const db = {
query: async <T>(sql: string, params?: any[]): Promise<T[]> => {
const logger = c.get('logger')
logger.info('Executing query', { sql, params })
// Simulated query execution
return [] as T[]
},
execute: async (sql: string, params?: any[]): Promise<void> => {
const logger = c.get('logger')
logger.info('Executing statement', { sql, params })
// Simulated execution
},
}
c.set('db', db)
await next()
})
// ============================================================================
// CACHE MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
// Simulated cache (use Redis, KV, etc. in production)
const cacheStore = new Map<string, { value: string; expiresAt: number }>()
const cache = {
get: async (key: string): Promise<string | null> => {
const logger = c.get('logger')
const entry = cacheStore.get(key)
if (!entry) {
logger.info('Cache miss', { key })
return null
}
if (entry.expiresAt < Date.now()) {
logger.info('Cache expired', { key })
cacheStore.delete(key)
return null
}
logger.info('Cache hit', { key })
return entry.value
},
set: async (key: string, value: string, ttl: number = 60000): Promise<void> => {
const logger = c.get('logger')
logger.info('Cache set', { key, ttl })
cacheStore.set(key, {
value,
expiresAt: Date.now() + ttl,
})
},
delete: async (key: string): Promise<void> => {
const logger = c.get('logger')
logger.info('Cache delete', { key })
cacheStore.delete(key)
},
}
c.set('cache', cache)
await next()
})
// ============================================================================
// AUTHENTICATION MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
const logger = c.get('logger')
if (!token) {
logger.warn('Missing authentication token')
return c.json({ error: 'Unauthorized' }, 401)
}
// Simulated token validation
if (token !== 'valid-token') {
logger.warn('Invalid authentication token')
return c.json({ error: 'Invalid token' }, 401)
}
// Simulated user lookup
const user = {
id: '123',
email: 'user@example.com',
name: 'John Doe',
role: 'user' as const,
}
c.set('user', user)
logger.info('User authenticated', { userId: user.id })
await next()
})
// ============================================================================
// ROUTES USING CONTEXT
// ============================================================================
// Route using logger
app.get('/api/log-example', (c) => {
const logger = c.get('logger')
logger.info('This is an info message')
logger.warn('This is a warning')
logger.error('This is an error')
return c.json({ message: 'Logged' })
})
// Route using user
app.get('/api/profile', (c) => {
const user = c.get('user')
return c.json({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
})
})
// Route using database
app.get('/api/users', async (c) => {
const db = c.get('db')
const logger = c.get('logger')
try {
const users = await db.query<{ id: string; name: string }>('SELECT * FROM users')
return c.json({ users })
} catch (error) {
logger.error('Database query failed', { error })
return c.json({ error: 'Database error' }, 500)
}
})
// Route using cache
app.get('/api/cached-data', async (c) => {
const cache = c.get('cache')
const logger = c.get('logger')
const cacheKey = 'expensive-data'
// Try to get from cache
const cached = await cache.get(cacheKey)
if (cached) {
return c.json({ data: JSON.parse(cached), cached: true })
}
// Simulate expensive computation
const data = { result: 'expensive data', timestamp: Date.now() }
// Store in cache
await cache.set(cacheKey, JSON.stringify(data), 60000) // 1 minute
return c.json({ data, cached: false })
})
// Route using request ID
app.get('/api/request-info', (c) => {
const requestId = c.get('requestId')
const startTime = c.get('startTime')
const elapsed = Date.now() - startTime
return c.json({
requestId,
elapsed: `${elapsed}ms`,
method: c.req.method,
path: c.req.path,
})
})
// Route using environment bindings
app.get('/api/env', (c) => {
const environment = c.env.ENVIRONMENT
const apiKey = c.env.API_KEY // Don't expose this in real app!
return c.json({
environment,
hasApiKey: !!apiKey,
})
})
// ============================================================================
// COMBINING MULTIPLE CONTEXT VALUES
// ============================================================================
app.post('/api/create-user', async (c) => {
const logger = c.get('logger')
const db = c.get('db')
const user = c.get('user')
const requestId = c.get('requestId')
// Check permissions
if (user.role !== 'admin') {
logger.warn('Unauthorized user creation attempt', {
userId: user.id,
requestId,
})
return c.json({ error: 'Forbidden' }, 403)
}
// Parse request body
const body = await c.req.json()
// Create user
try {
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', [body.name, body.email])
logger.info('User created', {
createdBy: user.id,
newUserEmail: body.email,
requestId,
})
return c.json({ success: true }, 201)
} catch (error) {
logger.error('User creation failed', {
error,
requestId,
})
return c.json({ error: 'Failed to create user' }, 500)
}
})
// ============================================================================
// CUSTOM CONTEXT HELPERS
// ============================================================================
// Helper to get authenticated user (with type guard)
function getAuthenticatedUser(c: Context<{ Bindings: Bindings; Variables: Variables }>) {
const user = c.get('user')
if (!user) {
throw new Error('User not authenticated')
}
return user
}
// Helper to check admin role
function requireAdmin(c: Context<{ Bindings: Bindings; Variables: Variables }>) {
const user = getAuthenticatedUser(c)
if (user.role !== 'admin') {
throw new Error('Admin access required')
}
return user
}
// Usage
app.delete('/api/users/:id', (c) => {
const admin = requireAdmin(c) // Throws if not admin
const logger = c.get('logger')
logger.info('User deletion requested', {
adminId: admin.id,
targetUserId: c.req.param('id'),
})
return c.json({ success: true })
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
export type { Bindings, Variables }
export { getAuthenticatedUser, requireAdmin }

409
templates/error-handling.ts Normal file
View File

@@ -0,0 +1,409 @@
/**
* Hono Error Handling
*
* Complete examples for error handling using HTTPException, onError, and custom error handlers.
*/
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// ============================================================================
// HTTPEXCEPTION - CLIENT ERRORS (400-499)
// ============================================================================
// 400 Bad Request
app.get('/bad-request', (c) => {
throw new HTTPException(400, { message: 'Bad Request - Invalid parameters' })
})
// 401 Unauthorized
app.get('/unauthorized', (c) => {
throw new HTTPException(401, { message: 'Unauthorized - Missing or invalid token' })
})
// 403 Forbidden
app.get('/forbidden', (c) => {
throw new HTTPException(403, { message: 'Forbidden - Insufficient permissions' })
})
// 404 Not Found
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
// Simulate database lookup
const user = null // await db.findUser(id)
if (!user) {
throw new HTTPException(404, { message: `User with ID ${id} not found` })
}
return c.json({ user })
})
// Custom response body
app.get('/custom-error', (c) => {
const res = new Response(
JSON.stringify({
error: 'CUSTOM_ERROR',
code: 'ERR001',
details: 'Custom error details',
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
)
throw new HTTPException(400, { res })
})
// ============================================================================
// AUTHENTICATION ERRORS
// ============================================================================
app.get('/protected', (c) => {
const token = c.req.header('Authorization')
if (!token) {
throw new HTTPException(401, {
message: 'Missing Authorization header',
})
}
if (!token.startsWith('Bearer ')) {
throw new HTTPException(401, {
message: 'Invalid Authorization header format',
})
}
const actualToken = token.replace('Bearer ', '')
if (actualToken !== 'valid-token') {
throw new HTTPException(401, {
message: 'Invalid or expired token',
})
}
return c.json({ message: 'Access granted', data: 'Protected data' })
})
// ============================================================================
// VALIDATION ERRORS
// ============================================================================
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(18),
})
// Validation errors automatically return 400
app.post('/users', zValidator('json', userSchema), (c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
})
// Custom validation error handler
app.post(
'/users/custom',
zValidator('json', userSchema, (result, c) => {
if (!result.success) {
throw new HTTPException(400, {
message: 'Validation failed',
cause: result.error,
})
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// ============================================================================
// GLOBAL ERROR HANDLER (onError)
// ============================================================================
app.onError((err, c) => {
console.error(`Error on ${c.req.method} ${c.req.path}:`, err)
// Handle HTTPException
if (err instanceof HTTPException) {
// Get custom response if provided
if (err.res) {
return err.res
}
// Return default HTTPException response
return c.json(
{
error: err.message,
status: err.status,
},
err.status
)
}
// Handle Zod validation errors
if (err.name === 'ZodError') {
return c.json(
{
error: 'Validation failed',
issues: err.issues,
},
400
)
}
// Handle unexpected errors (500)
return c.json(
{
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred',
},
500
)
})
// ============================================================================
// NOT FOUND HANDLER
// ============================================================================
app.notFound((c) => {
return c.json(
{
error: 'Not Found',
message: `Route ${c.req.method} ${c.req.path} not found`,
},
404
)
})
// ============================================================================
// MIDDLEWARE ERROR CHECKING
// ============================================================================
app.use('*', async (c, next) => {
await next()
// Check for errors after handler execution
if (c.error) {
console.error('Error detected in middleware:', {
error: c.error.message,
path: c.req.path,
method: c.req.method,
})
// Send to error tracking service
// await sendToSentry(c.error, { path: c.req.path, method: c.req.method })
}
})
// ============================================================================
// TRY-CATCH ERROR HANDLING
// ============================================================================
app.get('/external-api', async (c) => {
try {
// Simulated external API call
const response = await fetch('https://api.example.com/data')
if (!response.ok) {
throw new HTTPException(response.status, {
message: `External API returned ${response.status}`,
})
}
const data = await response.json()
return c.json({ data })
} catch (error) {
// Network errors or parsing errors
if (error instanceof HTTPException) {
throw error // Re-throw HTTPException
}
// Log unexpected error
console.error('External API error:', error)
// Return generic error to client
throw new HTTPException(503, {
message: 'External service unavailable',
})
}
})
// ============================================================================
// CONDITIONAL ERROR RESPONSES
// ============================================================================
app.get('/data/:id', async (c) => {
const id = c.req.param('id')
// Validate ID format
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
throw new HTTPException(400, {
message: 'Invalid UUID format',
})
}
// Check access permissions
const hasAccess = true // await checkUserAccess(id)
if (!hasAccess) {
throw new HTTPException(403, {
message: 'You do not have permission to access this resource',
})
}
// Fetch data
const data = null // await db.getData(id)
if (!data) {
throw new HTTPException(404, {
message: 'Resource not found',
})
}
return c.json({ data })
})
// ============================================================================
// TYPED ERROR RESPONSES
// ============================================================================
type ErrorResponse = {
error: string
code: string
details?: string
}
function createErrorResponse(code: string, message: string, details?: string): ErrorResponse {
return {
error: message,
code,
details,
}
}
app.get('/typed-error', (c) => {
const errorBody = createErrorResponse('USER_NOT_FOUND', 'User not found', 'The requested user does not exist')
throw new HTTPException(404, {
res: c.json(errorBody, 404),
})
})
// ============================================================================
// CUSTOM ERROR CLASSES
// ============================================================================
class ValidationError extends HTTPException {
constructor(message: string, cause?: any) {
super(400, { message, cause })
this.name = 'ValidationError'
}
}
class AuthenticationError extends HTTPException {
constructor(message: string) {
super(401, { message })
this.name = 'AuthenticationError'
}
}
class AuthorizationError extends HTTPException {
constructor(message: string) {
super(403, { message })
this.name = 'AuthorizationError'
}
}
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` })
this.name = 'NotFoundError'
}
}
// Usage
app.get('/custom-errors/:id', async (c) => {
const id = c.req.param('id')
if (!id) {
throw new ValidationError('ID is required')
}
const user = null // await db.findUser(id)
if (!user) {
throw new NotFoundError('User')
}
return c.json({ user })
})
// Handle custom error classes in onError
app.onError((err, c) => {
if (err instanceof ValidationError) {
return c.json({ error: err.message, type: 'validation' }, err.status)
}
if (err instanceof AuthenticationError) {
return c.json({ error: err.message, type: 'authentication' }, err.status)
}
if (err instanceof NotFoundError) {
return c.json({ error: err.message, type: 'not_found' }, err.status)
}
if (err instanceof HTTPException) {
return err.getResponse()
}
return c.json({ error: 'Internal Server Error' }, 500)
})
// ============================================================================
// ERROR LOGGING WITH CONTEXT
// ============================================================================
app.use('*', async (c, next) => {
const requestId = crypto.randomUUID()
c.set('requestId', requestId)
try {
await next()
} catch (error) {
// Log with context
console.error('Request failed', {
requestId,
method: c.req.method,
path: c.req.path,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
})
// Re-throw to be handled by onError
throw error
}
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
export {
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
createErrorResponse,
}

View File

@@ -0,0 +1,418 @@
/**
* Hono Middleware Composition
*
* Complete examples for middleware chaining, built-in middleware, and custom middleware.
*/
import { Hono } from 'hono'
import type { Next } from 'hono'
import type { Context } from 'hono'
// Built-in middleware
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'
import { etag } from 'hono/etag'
import { secureHeaders } from 'hono/secure-headers'
import { timing } from 'hono/timing'
const app = new Hono()
// ============================================================================
// BUILT-IN MIDDLEWARE
// ============================================================================
// Request logging (prints to console)
app.use('*', logger())
// CORS (for API routes)
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'],
maxAge: 600,
credentials: true,
})
)
// Pretty JSON (development only - adds indentation)
if (process.env.NODE_ENV === 'development') {
app.use('*', prettyJSON())
}
// Compression (gzip/deflate)
app.use('*', compress())
// Caching (HTTP cache headers)
app.use(
'/static/*',
cache({
cacheName: 'my-app',
cacheControl: 'max-age=3600',
})
)
// ETag support
app.use('/api/*', etag())
// Security headers
app.use('*', secureHeaders())
// Server timing header
app.use('*', timing())
// ============================================================================
// CUSTOM MIDDLEWARE
// ============================================================================
// Request ID middleware
const requestIdMiddleware = async (c: Context, next: Next) => {
const requestId = crypto.randomUUID()
c.set('requestId', requestId)
await next()
// Add to response headers
c.res.headers.set('X-Request-ID', requestId)
}
app.use('*', requestIdMiddleware)
// Performance timing middleware
const performanceMiddleware = async (c: Context, next: Next) => {
const start = Date.now()
await next()
const elapsed = Date.now() - start
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
console.log(`${c.req.method} ${c.req.path} - ${elapsed}ms`)
}
app.use('*', performanceMiddleware)
// Error logging middleware
const errorLoggerMiddleware = async (c: Context, next: Next) => {
await next()
// Check for errors after handler execution
if (c.error) {
console.error('Error occurred:', {
error: c.error.message,
stack: c.error.stack,
path: c.req.path,
method: c.req.method,
})
// Send to error tracking service (e.g., Sentry)
// await sendToErrorTracker(c.error, c.req)
}
}
app.use('*', errorLoggerMiddleware)
// ============================================================================
// AUTHENTICATION MIDDLEWARE
// ============================================================================
// Simple token authentication
const authMiddleware = async (c: Context, next: Next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Validate token (simplified example)
if (token !== 'secret-token') {
return c.json({ error: 'Invalid token' }, 401)
}
// Set user in context
c.set('user', {
id: 1,
name: 'John Doe',
email: 'john@example.com',
})
await next()
}
// Apply to specific routes
app.use('/admin/*', authMiddleware)
app.use('/api/protected/*', authMiddleware)
// ============================================================================
// RATE LIMITING MIDDLEWARE
// ============================================================================
// Simple in-memory rate limiter (production: use Redis/KV)
const rateLimits = new Map<string, { count: number; resetAt: number }>()
const rateLimitMiddleware = (maxRequests: number, windowMs: number) => {
return async (c: Context, next: Next) => {
const ip = c.req.header('CF-Connecting-IP') || 'unknown'
const now = Date.now()
const limit = rateLimits.get(ip)
if (!limit || limit.resetAt < now) {
// New window
rateLimits.set(ip, {
count: 1,
resetAt: now + windowMs,
})
} else if (limit.count >= maxRequests) {
// Rate limit exceeded
return c.json(
{
error: 'Too many requests',
retryAfter: Math.ceil((limit.resetAt - now) / 1000),
},
429
)
} else {
// Increment count
limit.count++
}
await next()
}
}
// Apply rate limiting (100 requests per minute)
app.use('/api/*', rateLimitMiddleware(100, 60000))
// ============================================================================
// MIDDLEWARE CHAINING
// ============================================================================
// Multiple middleware for specific route
app.get(
'/protected/data',
authMiddleware,
rateLimitMiddleware(10, 60000),
(c) => {
const user = c.get('user')
return c.json({ message: 'Protected data', user })
}
)
// ============================================================================
// CONDITIONAL MIDDLEWARE
// ============================================================================
// Apply middleware based on condition
const conditionalMiddleware = async (c: Context, next: Next) => {
const isDevelopment = process.env.NODE_ENV === 'development'
if (isDevelopment) {
console.log('[DEV]', c.req.method, c.req.path)
}
await next()
}
app.use('*', conditionalMiddleware)
// ============================================================================
// MIDDLEWARE FACTORY PATTERN
// ============================================================================
// Middleware factory for custom headers
const customHeadersMiddleware = (headers: Record<string, string>) => {
return async (c: Context, next: Next) => {
await next()
for (const [key, value] of Object.entries(headers)) {
c.res.headers.set(key, value)
}
}
}
// Apply custom headers
app.use(
'/api/*',
customHeadersMiddleware({
'X-API-Version': '1.0.0',
'X-Powered-By': 'Hono',
})
)
// ============================================================================
// CONTEXT EXTENSION MIDDLEWARE
// ============================================================================
// Logger middleware (extends context)
const loggerMiddleware = async (c: Context, next: Next) => {
const logger = {
info: (message: string) => console.log(`[INFO] ${message}`),
warn: (message: string) => console.warn(`[WARN] ${message}`),
error: (message: string) => console.error(`[ERROR] ${message}`),
}
c.set('logger', logger)
await next()
}
app.use('*', loggerMiddleware)
// Use logger in routes
app.get('/log-example', (c) => {
const logger = c.get('logger')
logger.info('This is an info message')
return c.json({ message: 'Logged' })
})
// ============================================================================
// DATABASE CONNECTION MIDDLEWARE
// ============================================================================
// Database connection (simplified example)
const dbMiddleware = async (c: Context, next: Next) => {
// Simulated database connection
const db = {
query: async (sql: string) => {
console.log('Executing query:', sql)
return []
},
close: async () => {
console.log('Closing database connection')
},
}
c.set('db', db)
await next()
// Cleanup
await db.close()
}
app.use('/api/*', dbMiddleware)
// ============================================================================
// REQUEST VALIDATION MIDDLEWARE
// ============================================================================
// Content-Type validation
const jsonOnlyMiddleware = async (c: Context, next: Next) => {
const contentType = c.req.header('Content-Type')
if (c.req.method === 'POST' || c.req.method === 'PUT') {
if (!contentType || !contentType.includes('application/json')) {
return c.json(
{
error: 'Content-Type must be application/json',
},
415
)
}
}
await next()
}
app.use('/api/*', jsonOnlyMiddleware)
// ============================================================================
// MIDDLEWARE EXECUTION ORDER
// ============================================================================
// Middleware runs in order: top to bottom before handler, bottom to top after
app.use('*', async (c, next) => {
console.log('1: Before handler')
await next()
console.log('6: After handler')
})
app.use('*', async (c, next) => {
console.log('2: Before handler')
await next()
console.log('5: After handler')
})
app.use('*', async (c, next) => {
console.log('3: Before handler')
await next()
console.log('4: After handler')
})
app.get('/middleware-order', (c) => {
console.log('Handler')
return c.json({ message: 'Check console for execution order' })
})
// Output: 1, 2, 3, Handler, 4, 5, 6
// ============================================================================
// EARLY RETURN FROM MIDDLEWARE
// ============================================================================
// Middleware can return early (short-circuit)
const maintenanceMiddleware = async (c: Context, next: Next) => {
const isMaintenanceMode = false // Set to true to enable
if (isMaintenanceMode) {
// Don't call next() - return response directly
return c.json(
{
error: 'Service is under maintenance',
retryAfter: 3600,
},
503
)
}
await next()
}
app.use('*', maintenanceMiddleware)
// ============================================================================
// TYPE-SAFE MIDDLEWARE
// ============================================================================
type Bindings = {
DATABASE_URL: string
API_KEY: string
}
type Variables = {
user: { id: number; name: string; email: string }
requestId: string
logger: {
info: (message: string) => void
error: (message: string) => void
}
db: {
query: (sql: string) => Promise<any[]>
close: () => Promise<void>
}
}
// Typed app
const typedApp = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// Type-safe middleware
typedApp.use('*', async (c, next) => {
const user = c.get('user') // Type-safe!
const logger = c.get('logger') // Type-safe!
await next()
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
export { typedApp }

33
templates/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "hono-app",
"version": "1.0.0",
"type": "module",
"description": "Hono application with routing, middleware, validation, and RPC",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"type-check": "tsc --noEmit"
},
"dependencies": {
"hono": "^4.10.2"
},
"devDependencies": {
"typescript": "^5.9.0",
"tsx": "^4.19.0",
"@types/node": "^22.10.0"
},
"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",
"arktype": "^2.0.0",
"typia": "^7.1.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -0,0 +1,299 @@
/**
* Hono Routing Patterns
*
* Complete examples for route parameters, query params, wildcards, and route grouping.
*/
import { Hono } from 'hono'
const app = new Hono()
// ============================================================================
// BASIC ROUTES
// ============================================================================
// GET request
app.get('/posts', (c) => {
return c.json({
posts: [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' },
],
})
})
// POST request
app.post('/posts', async (c) => {
const body = await c.req.json()
return c.json({ created: true, data: body }, 201)
})
// PUT request
app.put('/posts/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
return c.json({ updated: true, id, data: body })
})
// DELETE request
app.delete('/posts/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: true, id })
})
// Multiple methods on same route
app.on(['GET', 'POST'], '/multi', (c) => {
return c.text(`Method: ${c.req.method}`)
})
// All HTTP methods
app.all('/catch-all', (c) => {
return c.text(`Any method works: ${c.req.method}`)
})
// ============================================================================
// ROUTE PARAMETERS
// ============================================================================
// Single parameter
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({
userId: id,
name: 'John Doe',
})
})
// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({
postId,
commentId,
comment: 'This is a comment',
})
})
// Optional parameters (using wildcards)
app.get('/files/*', (c) => {
const path = c.req.param('*')
return c.json({
filePath: path || 'root',
message: 'File accessed',
})
})
// Named wildcard (regex pattern)
app.get('/assets/:filepath{.+}', (c) => {
const filepath = c.req.param('filepath')
return c.json({
asset: filepath,
contentType: 'application/octet-stream',
})
})
// ============================================================================
// QUERY PARAMETERS
// ============================================================================
// Single query param
app.get('/search', (c) => {
const q = c.req.query('q') // ?q=hello
return c.json({
query: q,
results: [],
})
})
// Multiple query params
app.get('/products', (c) => {
const page = c.req.query('page') || '1' // ?page=2
const limit = c.req.query('limit') || '10' // ?limit=20
const sort = c.req.query('sort') || 'name' // ?sort=price
return c.json({
page: parseInt(page, 10),
limit: parseInt(limit, 10),
sort,
products: [],
})
})
// All query params as object
app.get('/filter', (c) => {
const query = c.req.query()
return c.json({
filters: query,
results: [],
})
})
// Array query params (e.g., ?tag=js&tag=ts)
app.get('/tags', (c) => {
const tags = c.req.queries('tag') // returns string[]
return c.json({
tags: tags || [],
count: tags?.length || 0,
})
})
// ============================================================================
// WILDCARD ROUTES
// ============================================================================
// Catch-all route (must be last)
app.get('/api/*', (c) => {
const path = c.req.param('*')
return c.json({
message: 'API catch-all',
requestedPath: path,
})
})
// Multiple wildcard levels
app.get('/cdn/:version/*', (c) => {
const version = c.req.param('version')
const path = c.req.param('*')
return c.json({
version,
assetPath: path,
})
})
// ============================================================================
// ROUTE GROUPING (SUB-APPS)
// ============================================================================
// Create API sub-app
const api = new Hono()
api.get('/users', (c) => {
return c.json({ users: [] })
})
api.get('/posts', (c) => {
return c.json({ posts: [] })
})
api.get('/comments', (c) => {
return c.json({ comments: [] })
})
// Create admin sub-app
const admin = new Hono()
admin.get('/dashboard', (c) => {
return c.json({ message: 'Admin Dashboard' })
})
admin.get('/users', (c) => {
return c.json({ message: 'Admin Users' })
})
// Mount sub-apps
app.route('/api', api) // Routes: /api/users, /api/posts, /api/comments
app.route('/admin', admin) // Routes: /admin/dashboard, /admin/users
// ============================================================================
// ROUTE CHAINING
// ============================================================================
// Method chaining for same path
app
.get('/items', (c) => c.json({ items: [] }))
.post('/items', (c) => c.json({ created: true }))
.put('/items/:id', (c) => c.json({ updated: true }))
.delete('/items/:id', (c) => c.json({ deleted: true }))
// ============================================================================
// ROUTE PRIORITY
// ============================================================================
// Specific routes BEFORE wildcards
app.get('/special/exact', (c) => {
return c.json({ message: 'Exact route' })
})
app.get('/special/*', (c) => {
return c.json({ message: 'Wildcard route' })
})
// Request to /special/exact → "Exact route"
// Request to /special/anything → "Wildcard route"
// ============================================================================
// HEADER AND BODY ACCESS
// ============================================================================
// Accessing headers
app.get('/headers', (c) => {
const userAgent = c.req.header('User-Agent')
const authorization = c.req.header('Authorization')
// All headers
const allHeaders = c.req.raw.headers
return c.json({
userAgent,
authorization,
allHeaders: Object.fromEntries(allHeaders.entries()),
})
})
// Accessing request body
app.post('/body', async (c) => {
// JSON body
const json = await c.req.json()
// Text body
// const text = await c.req.text()
// Form data
// const formData = await c.req.formData()
// Array buffer
// const buffer = await c.req.arrayBuffer()
return c.json({ received: json })
})
// ============================================================================
// ROUTE METADATA
// ============================================================================
// Access current route information
app.get('/info', (c) => {
return c.json({
method: c.req.method, // GET
url: c.req.url, // Full URL
path: c.req.path, // Path only
routePath: c.req.routePath, // Route pattern (e.g., /info)
})
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
// TypeScript types for environment
type Bindings = {
// Add your environment variables here
}
type Variables = {
// Add your context variables here
}
// Typed app
export const typedApp = new Hono<{ Bindings: Bindings; Variables: Variables }>()

268
templates/rpc-client.ts Normal file
View File

@@ -0,0 +1,268 @@
/**
* Hono RPC Client
*
* Type-safe client for consuming Hono APIs with full type inference.
*/
import { hc } from 'hono/client'
import type { AppType, PostsType, SearchType } from './rpc-pattern'
// ============================================================================
// BASIC CLIENT SETUP
// ============================================================================
// Create client with full type inference
const client = hc<AppType>('http://localhost:8787')
// ============================================================================
// TYPE-SAFE API CALLS
// ============================================================================
async function exampleUsage() {
// GET /users
const usersRes = await client.users.$get()
const usersData = await usersRes.json()
// Type: { users: Array<{ id: string, name: string, email: string }> }
console.log('Users:', usersData.users)
// POST /users
const createRes = await client.users.$post({
json: {
name: 'Charlie',
email: 'charlie@example.com',
age: 25,
},
})
if (!createRes.ok) {
console.error('Failed to create user:', createRes.status)
return
}
const createData = await createRes.json()
// Type: { success: boolean, user: { id: string, name: string, email: string, age?: number } }
console.log('Created user:', createData.user)
// GET /users/:id
const userRes = await client.users[':id'].$get({
param: { id: createData.user.id },
})
const userData = await userRes.json()
// Type: { id: string, name: string, email: string }
console.log('User:', userData)
// PATCH /users/:id
const updateRes = await client.users[':id'].$patch({
param: { id: userData.id },
json: {
name: 'Charlie Updated',
},
})
const updateData = await updateRes.json()
console.log('Updated user:', updateData)
// DELETE /users/:id
const deleteRes = await client.users[':id'].$delete({
param: { id: userData.id },
})
const deleteData = await deleteRes.json()
console.log('Deleted user:', deleteData)
}
// ============================================================================
// QUERY PARAMETERS
// ============================================================================
const searchClient = hc<SearchType>('http://localhost:8787')
async function searchExample() {
const res = await searchClient.search.$get({
query: {
q: 'hello',
page: '2', // Converted to number by schema
},
})
const data = await res.json()
// Type: { query: string, page: number, results: any[] }
console.log('Search results:', data)
}
// ============================================================================
// ERROR HANDLING
// ============================================================================
async function errorHandlingExample() {
try {
const res = await client.users.$post({
json: {
name: '',
email: 'invalid-email',
},
})
if (!res.ok) {
// Handle validation error (400)
if (res.status === 400) {
const error = await res.json()
console.error('Validation error:', error)
return
}
// Handle other errors
console.error('Request failed:', res.status, res.statusText)
return
}
const data = await res.json()
console.log('Success:', data)
} catch (error) {
console.error('Network error:', error)
}
}
// ============================================================================
// CUSTOM HEADERS
// ============================================================================
async function authExample() {
const authedClient = hc<AppType>('http://localhost:8787', {
headers: {
Authorization: 'Bearer secret-token',
'X-API-Version': '1.0',
},
})
const res = await authedClient.users.$get()
const data = await res.json()
console.log('Authed response:', data)
}
// ============================================================================
// FETCH OPTIONS
// ============================================================================
async function fetchOptionsExample() {
const res = await client.users.$get({}, {
// Standard fetch options
headers: {
'X-Custom-Header': 'value',
},
signal: AbortSignal.timeout(5000), // 5 second timeout
})
const data = await res.json()
console.log('Data:', data)
}
// ============================================================================
// GROUPED ROUTES CLIENT
// ============================================================================
const postsClient = hc<PostsType>('http://localhost:8787/posts')
async function postsExample() {
// GET /posts
const postsRes = await postsClient.index.$get()
const posts = await postsRes.json()
console.log('Posts:', posts)
// POST /posts
const createRes = await postsClient.index.$post({
json: {
title: 'My Post',
content: 'Post content here',
},
})
const newPost = await createRes.json()
console.log('Created post:', newPost)
}
// ============================================================================
// FRONTEND USAGE (REACT EXAMPLE)
// ============================================================================
import { useState, useEffect } from 'react'
function UsersComponent() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
async function fetchUsers() {
setLoading(true)
try {
const res = await client.users.$get()
const data = await res.json()
setUsers(data.users)
} catch (error) {
console.error('Failed to fetch users:', error)
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
async function createUser(name: string, email: string) {
try {
const res = await client.users.$post({
json: { name, email },
})
if (!res.ok) {
alert('Failed to create user')
return
}
const data = await res.json()
setUsers([...users, data.user])
} catch (error) {
console.error('Failed to create user:', error)
}
}
if (loading) return <div>Loading...</div>
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
)
}
// ============================================================================
// EXPORT
// ============================================================================
export {
client,
searchClient,
postsClient,
exampleUsage,
searchExample,
errorHandlingExample,
authExample,
fetchOptionsExample,
postsExample,
UsersComponent,
}

202
templates/rpc-pattern.ts Normal file
View File

@@ -0,0 +1,202 @@
/**
* Hono RPC Pattern
*
* Type-safe client/server communication using Hono's RPC feature.
*/
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
// ============================================================================
// SERVER-SIDE: Define Routes with Type Export
// ============================================================================
const app = new Hono()
// Define schemas
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(18).optional(),
})
const updateUserSchema = createUserSchema.partial()
const userParamSchema = z.object({
id: z.string().uuid(),
})
// ============================================================================
// METHOD 1: Export Individual Routes
// ============================================================================
// Define route and assign to variable (REQUIRED for RPC type inference)
const getUsers = app.get('/users', (c) => {
return c.json({
users: [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
],
})
})
const createUser = app.post('/users', zValidator('json', createUserSchema), (c) => {
const data = c.req.valid('json')
return c.json(
{
success: true,
user: {
id: crypto.randomUUID(),
...data,
},
},
201
)
})
const getUser = app.get('/users/:id', zValidator('param', userParamSchema), (c) => {
const { id } = c.req.valid('param')
return c.json({
id,
name: 'Alice',
email: 'alice@example.com',
})
})
const updateUser = app.patch(
'/users/:id',
zValidator('param', userParamSchema),
zValidator('json', updateUserSchema),
(c) => {
const { id } = c.req.valid('param')
const updates = c.req.valid('json')
return c.json({
success: true,
user: {
id,
...updates,
},
})
}
)
const deleteUser = app.delete('/users/:id', zValidator('param', userParamSchema), (c) => {
const { id } = c.req.valid('param')
return c.json({ success: true, deletedId: id })
})
// Export combined type for RPC client
export type AppType = typeof getUsers | typeof createUser | typeof getUser | typeof updateUser | typeof deleteUser
// ============================================================================
// METHOD 2: Export Entire App (Simpler but slower for large apps)
// ============================================================================
const simpleApp = new Hono()
simpleApp.get('/hello', (c) => {
return c.json({ message: 'Hello!' })
})
simpleApp.post('/echo', zValidator('json', z.object({ message: z.string() })), (c) => {
const { message } = c.req.valid('json')
return c.json({ echo: message })
})
export type SimpleAppType = typeof simpleApp
// ============================================================================
// METHOD 3: Group Routes by Domain
// ============================================================================
// Posts routes
const postsApp = new Hono()
const getPosts = postsApp.get('/', (c) => {
return c.json({ posts: [] })
})
const createPost = postsApp.post(
'/',
zValidator(
'json',
z.object({
title: z.string(),
content: z.string(),
})
),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, post: { id: '1', ...data } }, 201)
}
)
export type PostsType = typeof getPosts | typeof createPost
// Comments routes
const commentsApp = new Hono()
const getComments = commentsApp.get('/', (c) => {
return c.json({ comments: [] })
})
export type CommentsType = typeof getComments
// Mount to main app
app.route('/posts', postsApp)
app.route('/comments', commentsApp)
// ============================================================================
// MIDDLEWARE WITH RPC
// ============================================================================
// Middleware that returns early (short-circuits)
const authMiddleware = async (c: any, next: any) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
}
// Route with middleware
const protectedRoute = app.get('/protected', authMiddleware, (c) => {
return c.json({ data: 'Protected data' })
})
// Export type includes middleware responses
export type ProtectedType = typeof protectedRoute
// ============================================================================
// QUERY PARAMETER HANDLING
// ============================================================================
const searchQuerySchema = z.object({
q: z.string(),
page: z.string().transform((val) => parseInt(val, 10)),
})
const searchRoute = app.get('/search', zValidator('query', searchQuerySchema), (c) => {
const { q, page } = c.req.valid('query')
return c.json({
query: q,
page,
results: [],
})
})
export type SearchType = typeof searchRoute
// ============================================================================
// EXPORT MAIN APP
// ============================================================================
export default app

View File

@@ -0,0 +1,315 @@
/**
* Hono Validation with Valibot
*
* Complete examples for request validation using Valibot validator.
* Valibot is lighter and faster than Zod, with modular imports.
*/
import { Hono } from 'hono'
import { vValidator } from '@hono/valibot-validator'
import * as v from 'valibot'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// ============================================================================
// BASIC VALIDATION
// ============================================================================
// JSON body validation
const userSchema = 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', userSchema), (c) => {
const data = c.req.valid('json') // Type-safe!
return c.json({
success: true,
data,
})
})
// ============================================================================
// QUERY PARAMETER VALIDATION
// ============================================================================
const searchSchema = v.object({
q: v.pipe(v.string(), v.minLength(1)),
page: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
limit: v.optional(
v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1), v.maxValue(100)),
'10'
),
sort: v.optional(v.picklist(['name', 'date', 'relevance'])),
})
app.get('/search', vValidator('query', searchSchema), (c) => {
const { q, page, limit, sort } = c.req.valid('query')
return c.json({
query: q,
page,
limit,
sort,
results: [],
})
})
// ============================================================================
// ROUTE PARAMETER VALIDATION
// ============================================================================
// UUID parameter
const uuidParamSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
})
app.get('/users/:id', vValidator('param', uuidParamSchema), (c) => {
const { id } = c.req.valid('param')
return c.json({
userId: id,
name: 'John Doe',
})
})
// Numeric ID parameter
const numericIdSchema = v.object({
id: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
})
app.get('/posts/:id', vValidator('param', numericIdSchema), (c) => {
const { id } = c.req.valid('param') // Type: number
return c.json({
postId: id,
title: 'Post Title',
})
})
// ============================================================================
// HEADER VALIDATION
// ============================================================================
const headerSchema = v.object({
'authorization': v.pipe(v.string(), v.startsWith('Bearer ')),
'content-type': v.literal('application/json'),
'x-api-version': v.optional(v.picklist(['1.0', '2.0'])),
})
app.post('/auth', vValidator('header', headerSchema), (c) => {
const headers = c.req.valid('header')
return c.json({
authenticated: true,
apiVersion: headers['x-api-version'] || '1.0',
})
})
// ============================================================================
// CUSTOM VALIDATION HOOKS
// ============================================================================
const customErrorSchema = v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.integer(), v.minValue(18)),
})
app.post(
'/register',
vValidator('json', customErrorSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
issues: result.issues,
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// ============================================================================
// COMPLEX SCHEMA VALIDATION
// ============================================================================
const addressSchema = v.object({
street: v.string(),
city: v.string(),
zipCode: v.pipe(v.string(), v.regex(/^\d{5}$/)),
country: v.pipe(v.string(), v.length(2)),
})
const profileSchema = v.object({
name: v.string(),
email: v.pipe(v.string(), v.email()),
address: addressSchema,
tags: v.pipe(v.array(v.string()), v.minLength(1), v.maxLength(5)),
})
app.post('/profile', vValidator('json', profileSchema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
profile: data,
})
})
// ============================================================================
// DISCRIMINATED UNION
// ============================================================================
const paymentSchema = v.variant('method', [
v.object({
method: v.literal('card'),
cardNumber: v.pipe(v.string(), v.regex(/^\d{16}$/)),
cvv: v.pipe(v.string(), v.regex(/^\d{3}$/)),
}),
v.object({
method: v.literal('paypal'),
email: v.pipe(v.string(), v.email()),
}),
v.object({
method: v.literal('bank'),
accountNumber: v.string(),
routingNumber: v.string(),
}),
])
app.post('/payment', vValidator('json', paymentSchema), (c) => {
const payment = c.req.valid('json')
if (payment.method === 'card') {
console.log('Processing card payment:', payment.cardNumber)
}
return c.json({ success: true })
})
// ============================================================================
// TRANSFORMATIONS
// ============================================================================
const eventSchema = v.object({
name: v.string(),
startDate: v.pipe(v.string(), v.transform((val) => new Date(val))),
endDate: v.pipe(v.string(), v.transform((val) => new Date(val))),
capacity: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
})
app.post('/events', vValidator('json', eventSchema), (c) => {
const event = c.req.valid('json')
return c.json({
success: true,
event: {
...event,
startDate: event.startDate.toISOString(),
endDate: event.endDate.toISOString(),
},
})
})
// ============================================================================
// OPTIONAL AND DEFAULT VALUES
// ============================================================================
const settingsSchema = v.object({
theme: v.optional(v.picklist(['light', 'dark']), 'light'),
notifications: v.optional(v.boolean()),
language: v.optional(v.string(), 'en'),
maxResults: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100)), 10),
})
app.post('/settings', vValidator('json', settingsSchema), (c) => {
const settings = c.req.valid('json')
return c.json({
success: true,
settings,
})
})
// ============================================================================
// ARRAY VALIDATION
// ============================================================================
const batchCreateSchema = v.object({
users: v.pipe(
v.array(
v.object({
name: v.string(),
email: v.pipe(v.string(), v.email()),
})
),
v.minLength(1),
v.maxLength(100)
),
})
app.post('/batch-users', vValidator('json', batchCreateSchema), (c) => {
const { users } = c.req.valid('json')
return c.json({
success: true,
count: users.length,
users,
})
})
// ============================================================================
// PARTIAL SCHEMAS
// ============================================================================
const updateUserSchema = v.partial(userSchema)
app.patch('/users/:id', vValidator('json', updateUserSchema), (c) => {
const updates = c.req.valid('json')
return c.json({
success: true,
updated: updates,
})
})
// ============================================================================
// UNION TYPES
// ============================================================================
const contentSchema = v.variant('type', [
v.object({ type: v.literal('text'), content: v.string() }),
v.object({ type: v.literal('image'), url: v.pipe(v.string(), v.url()) }),
v.object({ type: v.literal('video'), videoId: v.string() }),
])
app.post('/content', vValidator('json', contentSchema), (c) => {
const content = c.req.valid('json')
return c.json({
success: true,
contentType: content.type,
})
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
export {
userSchema,
searchSchema,
uuidParamSchema,
profileSchema,
paymentSchema,
}

428
templates/validation-zod.ts Normal file
View File

@@ -0,0 +1,428 @@
/**
* Hono Validation with Zod
*
* Complete examples for request validation using Zod validator.
*/
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// ============================================================================
// BASIC VALIDATION
// ============================================================================
// JSON body validation
const userSchema = 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', userSchema), (c) => {
const data = c.req.valid('json') // Type-safe! { name: string, email: string, age?: number }
return c.json({
success: true,
data,
})
})
// ============================================================================
// QUERY PARAMETER VALIDATION
// ============================================================================
// Search query validation
const searchSchema = z.object({
q: z.string().min(1),
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))
.optional()
.default('10'),
sort: z.enum(['name', 'date', 'relevance']).optional(),
})
app.get('/search', zValidator('query', searchSchema), (c) => {
const { q, page, limit, sort } = c.req.valid('query')
// All types inferred correctly!
return c.json({
query: q,
page,
limit,
sort,
results: [],
})
})
// ============================================================================
// ROUTE PARAMETER VALIDATION
// ============================================================================
// UUID parameter validation
const uuidParamSchema = z.object({
id: z.string().uuid(),
})
app.get('/users/:id', zValidator('param', uuidParamSchema), (c) => {
const { id } = c.req.valid('param') // Type: string (validated UUID)
return c.json({
userId: id,
name: 'John Doe',
})
})
// Numeric ID parameter
const numericIdSchema = z.object({
id: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
})
app.get('/posts/:id', zValidator('param', numericIdSchema), (c) => {
const { id } = c.req.valid('param') // Type: number
return c.json({
postId: id,
title: 'Post Title',
})
})
// ============================================================================
// HEADER VALIDATION
// ============================================================================
const headerSchema = z.object({
'authorization': z.string().startsWith('Bearer '),
'content-type': z.literal('application/json'),
'x-api-version': z.enum(['1.0', '2.0']).optional(),
})
app.post('/auth', zValidator('header', headerSchema), (c) => {
const headers = c.req.valid('header')
return c.json({
authenticated: true,
apiVersion: headers['x-api-version'] || '1.0',
})
})
// ============================================================================
// FORM DATA VALIDATION
// ============================================================================
const formSchema = z.object({
username: z.string().min(3),
password: z.string().min(8),
remember: z.enum(['on', 'off']).optional(),
})
app.post('/login', zValidator('form', formSchema), (c) => {
const { username, password, remember } = c.req.valid('form')
return c.json({
loggedIn: true,
username,
rememberMe: remember === 'on',
})
})
// ============================================================================
// CUSTOM VALIDATION HOOKS
// ============================================================================
// Custom error response
const customErrorSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
})
app.post(
'/register',
zValidator('json', customErrorSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
issues: result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
})),
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// Throw HTTPException on validation failure
app.post(
'/submit',
zValidator('json', userSchema, (result, c) => {
if (!result.success) {
throw new HTTPException(400, {
message: 'Validation error',
cause: result.error,
})
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// ============================================================================
// COMPLEX SCHEMA VALIDATION
// ============================================================================
// Nested objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2),
})
const profileSchema = z.object({
name: z.string(),
email: z.string().email(),
address: addressSchema,
tags: z.array(z.string()).min(1).max(5),
})
app.post('/profile', zValidator('json', profileSchema), (c) => {
const data = c.req.valid('json')
// Fully typed nested structure!
return c.json({
success: true,
profile: data,
})
})
// ============================================================================
// CONDITIONAL VALIDATION
// ============================================================================
// Discriminated union
const paymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('card'),
cardNumber: z.string().regex(/^\d{16}$/),
cvv: z.string().regex(/^\d{3}$/),
}),
z.object({
method: z.literal('paypal'),
email: z.string().email(),
}),
z.object({
method: z.literal('bank'),
accountNumber: z.string(),
routingNumber: z.string(),
}),
])
app.post('/payment', zValidator('json', paymentSchema), (c) => {
const payment = c.req.valid('json')
// TypeScript knows the shape based on payment.method
if (payment.method === 'card') {
console.log('Processing card payment:', payment.cardNumber)
}
return c.json({ success: true })
})
// ============================================================================
// REFINEMENTS AND CUSTOM VALIDATION
// ============================================================================
// Custom validation logic
const passwordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
})
app.post('/change-password', zValidator('json', passwordSchema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
message: 'Password changed',
})
})
// ============================================================================
// TRANSFORMATION AND COERCION
// ============================================================================
// Transform strings to dates
const eventSchema = z.object({
name: z.string(),
startDate: z.string().transform((val) => new Date(val)),
endDate: z.string().transform((val) => new Date(val)),
capacity: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
})
app.post('/events', zValidator('json', eventSchema), (c) => {
const event = c.req.valid('json')
// event.startDate is Date, not string!
return c.json({
success: true,
event: {
...event,
startDate: event.startDate.toISOString(),
endDate: event.endDate.toISOString(),
},
})
})
// ============================================================================
// OPTIONAL AND DEFAULT VALUES
// ============================================================================
const settingsSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
notifications: z.boolean().optional(),
language: z.string().default('en'),
maxResults: z.number().int().min(1).max(100).default(10),
})
app.post('/settings', zValidator('json', settingsSchema), (c) => {
const settings = c.req.valid('json')
// Default values applied if not provided
return c.json({
success: true,
settings,
})
})
// ============================================================================
// ARRAY VALIDATION
// ============================================================================
const batchCreateSchema = z.object({
users: z.array(
z.object({
name: z.string(),
email: z.string().email(),
})
).min(1).max(100),
})
app.post('/batch-users', zValidator('json', batchCreateSchema), (c) => {
const { users } = c.req.valid('json')
return c.json({
success: true,
count: users.length,
users,
})
})
// ============================================================================
// MULTIPLE VALIDATORS
// ============================================================================
// Validate multiple targets
const headerAuthSchema = z.object({
authorization: z.string().startsWith('Bearer '),
})
const bodyDataSchema = z.object({
data: z.string(),
})
app.post(
'/protected',
zValidator('header', headerAuthSchema),
zValidator('json', bodyDataSchema),
(c) => {
const headers = c.req.valid('header')
const body = c.req.valid('json')
return c.json({
authenticated: true,
data: body.data,
})
}
)
// ============================================================================
// PARTIAL AND PICK SCHEMAS
// ============================================================================
// Update endpoint - make all fields optional
const updateUserSchema = userSchema.partial()
app.patch('/users/:id', zValidator('json', updateUserSchema), (c) => {
const updates = c.req.valid('json')
// All fields are optional
return c.json({
success: true,
updated: updates,
})
})
// Pick specific fields
const loginSchema = userSchema.pick({ email: true })
app.post('/login', zValidator('json', loginSchema), (c) => {
const { email } = c.req.valid('json')
return c.json({
success: true,
email,
})
})
// ============================================================================
// UNION AND INTERSECTION
// ============================================================================
// Union types (either/or)
const contentSchema = z.union([
z.object({ type: z.literal('text'), content: z.string() }),
z.object({ type: z.literal('image'), url: z.string().url() }),
z.object({ type: z.literal('video'), videoId: z.string() }),
])
app.post('/content', zValidator('json', contentSchema), (c) => {
const content = c.req.valid('json')
return c.json({
success: true,
contentType: content.type,
})
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
// Export schemas for reuse
export {
userSchema,
searchSchema,
uuidParamSchema,
profileSchema,
paymentSchema,
}