Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
|
||||
101
plugin.lock.json
Normal file
101
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
585
references/middleware-catalog.md
Normal file
585
references/middleware-catalog.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# Hono Built-in Middleware Catalog
|
||||
|
||||
Complete reference for all built-in Hono middleware with usage examples and configuration options.
|
||||
|
||||
**Last Updated**: 2025-10-22
|
||||
**Hono Version**: 4.10.2+
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
All built-in middleware are included in the `hono` package. No additional dependencies required.
|
||||
|
||||
```bash
|
||||
npm install hono@4.10.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Logging
|
||||
|
||||
### logger()
|
||||
|
||||
Logs request method, path, status, and response time.
|
||||
|
||||
```typescript
|
||||
import { logger } from 'hono/logger'
|
||||
|
||||
app.use('*', logger())
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
GET /api/users 200 - 15ms
|
||||
POST /api/posts 201 - 42ms
|
||||
```
|
||||
|
||||
**Custom logger**:
|
||||
```typescript
|
||||
import { logger } from 'hono/logger'
|
||||
|
||||
app.use(
|
||||
'*',
|
||||
logger((message, ...rest) => {
|
||||
console.log(`[Custom] ${message}`, ...rest)
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CORS
|
||||
|
||||
### cors()
|
||||
|
||||
Enables Cross-Origin Resource Sharing.
|
||||
|
||||
```typescript
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
// Simple usage
|
||||
app.use('*', cors())
|
||||
|
||||
// Custom configuration
|
||||
app.use(
|
||||
'/api/*',
|
||||
cors({
|
||||
origin: ['https://example.com', 'https://app.example.com'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
exposeHeaders: ['X-Request-ID', 'X-Response-Time'],
|
||||
maxAge: 600,
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
|
||||
// Dynamic origin
|
||||
app.use(
|
||||
'*',
|
||||
cors({
|
||||
origin: (origin) => {
|
||||
return origin.endsWith('.example.com') ? origin : 'https://example.com'
|
||||
},
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `origin`: String, array, or function
|
||||
- `allowMethods`: HTTP methods array
|
||||
- `allowHeaders`: Headers array
|
||||
- `exposeHeaders`: Headers to expose
|
||||
- `maxAge`: Preflight cache duration (seconds)
|
||||
- `credentials`: Allow credentials
|
||||
|
||||
---
|
||||
|
||||
## Pretty JSON
|
||||
|
||||
### prettyJSON()
|
||||
|
||||
Formats JSON responses with indentation (development only).
|
||||
|
||||
```typescript
|
||||
import { prettyJSON } from 'hono/pretty-json'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use('*', prettyJSON())
|
||||
}
|
||||
```
|
||||
|
||||
**Options**:
|
||||
```typescript
|
||||
app.use(
|
||||
'*',
|
||||
prettyJSON({
|
||||
space: 2, // Indentation spaces (default: 2)
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compression
|
||||
|
||||
### compress()
|
||||
|
||||
Compresses responses using gzip or deflate.
|
||||
|
||||
```typescript
|
||||
import { compress } from 'hono/compress'
|
||||
|
||||
app.use('*', compress())
|
||||
|
||||
// Custom options
|
||||
app.use(
|
||||
'*',
|
||||
compress({
|
||||
encoding: 'gzip', // 'gzip' | 'deflate'
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Automatically detects `Accept-Encoding` header
|
||||
- Skips if `Content-Encoding` already set
|
||||
- Skips if response is already compressed
|
||||
|
||||
---
|
||||
|
||||
## Caching
|
||||
|
||||
### cache()
|
||||
|
||||
Sets HTTP cache headers.
|
||||
|
||||
```typescript
|
||||
import { cache } from 'hono/cache'
|
||||
|
||||
app.use(
|
||||
'/static/*',
|
||||
cache({
|
||||
cacheName: 'my-app',
|
||||
cacheControl: 'max-age=3600', // 1 hour
|
||||
})
|
||||
)
|
||||
|
||||
// Conditional caching
|
||||
app.use(
|
||||
'/api/public/*',
|
||||
cache({
|
||||
cacheName: 'api-cache',
|
||||
cacheControl: 'public, max-age=300', // 5 minutes
|
||||
wait: true, // Wait for cache to be ready
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `cacheName`: Cache name
|
||||
- `cacheControl`: Cache-Control header value
|
||||
- `wait`: Wait for cache to be ready
|
||||
|
||||
---
|
||||
|
||||
## ETag
|
||||
|
||||
### etag()
|
||||
|
||||
Generates and validates ETags for responses.
|
||||
|
||||
```typescript
|
||||
import { etag } from 'hono/etag'
|
||||
|
||||
app.use('/api/*', etag())
|
||||
|
||||
// Custom options
|
||||
app.use(
|
||||
'/api/*',
|
||||
etag({
|
||||
weak: true, // Use weak ETags (W/"...")
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Automatically generates ETag from response body
|
||||
- Returns 304 Not Modified if ETag matches
|
||||
- Works with `If-None-Match` header
|
||||
|
||||
---
|
||||
|
||||
## Security Headers
|
||||
|
||||
### secureHeaders()
|
||||
|
||||
Sets security-related HTTP headers.
|
||||
|
||||
```typescript
|
||||
import { secureHeaders } from 'hono/secure-headers'
|
||||
|
||||
app.use('*', secureHeaders())
|
||||
|
||||
// Custom configuration
|
||||
app.use(
|
||||
'*',
|
||||
secureHeaders({
|
||||
contentSecurityPolicy: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
},
|
||||
strictTransportSecurity: 'max-age=31536000; includeSubDomains',
|
||||
xFrameOptions: 'DENY',
|
||||
xContentTypeOptions: 'nosniff',
|
||||
referrerPolicy: 'no-referrer',
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
**Headers set**:
|
||||
- `Content-Security-Policy`
|
||||
- `Strict-Transport-Security`
|
||||
- `X-Frame-Options`
|
||||
- `X-Content-Type-Options`
|
||||
- `Referrer-Policy`
|
||||
- `X-XSS-Protection` (deprecated but included)
|
||||
|
||||
---
|
||||
|
||||
## Server Timing
|
||||
|
||||
### timing()
|
||||
|
||||
Adds Server-Timing header with performance metrics.
|
||||
|
||||
```typescript
|
||||
import { timing } from 'hono/timing'
|
||||
|
||||
app.use('*', timing())
|
||||
|
||||
// Custom timing
|
||||
import { setMetric, startTime, endTime } from 'hono/timing'
|
||||
|
||||
app.use('*', timing())
|
||||
|
||||
app.get('/api/data', async (c) => {
|
||||
startTime(c, 'db')
|
||||
// Database query
|
||||
endTime(c, 'db')
|
||||
|
||||
startTime(c, 'external')
|
||||
// External API call
|
||||
endTime(c, 'external')
|
||||
|
||||
setMetric(c, 'total', 150)
|
||||
|
||||
return c.json({ data: [] })
|
||||
})
|
||||
```
|
||||
|
||||
**Output header**:
|
||||
```
|
||||
Server-Timing: db;dur=45, external;dur=85, total;dur=150
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bearer Auth
|
||||
|
||||
### bearerAuth()
|
||||
|
||||
Simple bearer token authentication.
|
||||
|
||||
```typescript
|
||||
import { bearerAuth } from 'hono/bearer-auth'
|
||||
|
||||
app.use(
|
||||
'/admin/*',
|
||||
bearerAuth({
|
||||
token: 'my-secret-token',
|
||||
})
|
||||
)
|
||||
|
||||
// Multiple tokens
|
||||
app.use(
|
||||
'/api/*',
|
||||
bearerAuth({
|
||||
token: ['token1', 'token2', 'token3'],
|
||||
})
|
||||
)
|
||||
|
||||
// Custom error
|
||||
app.use(
|
||||
'/api/*',
|
||||
bearerAuth({
|
||||
token: 'secret',
|
||||
realm: 'My API',
|
||||
onError: (c) => {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
},
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Auth
|
||||
|
||||
### basicAuth()
|
||||
|
||||
HTTP Basic authentication.
|
||||
|
||||
```typescript
|
||||
import { basicAuth } from 'hono/basic-auth'
|
||||
|
||||
app.use(
|
||||
'/admin/*',
|
||||
basicAuth({
|
||||
username: 'admin',
|
||||
password: 'secret',
|
||||
})
|
||||
)
|
||||
|
||||
// Custom validation
|
||||
app.use(
|
||||
'/api/*',
|
||||
basicAuth({
|
||||
verifyUser: async (username, password, c) => {
|
||||
const user = await db.findUser(username)
|
||||
if (!user) return false
|
||||
|
||||
return await bcrypt.compare(password, user.passwordHash)
|
||||
},
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JWT
|
||||
|
||||
### jwt()
|
||||
|
||||
JSON Web Token authentication.
|
||||
|
||||
```typescript
|
||||
import { jwt } from 'hono/jwt'
|
||||
|
||||
app.use(
|
||||
'/api/*',
|
||||
jwt({
|
||||
secret: 'my-secret-key',
|
||||
})
|
||||
)
|
||||
|
||||
// Access JWT payload
|
||||
app.get('/api/profile', (c) => {
|
||||
const payload = c.get('jwtPayload')
|
||||
return c.json({ user: payload })
|
||||
})
|
||||
|
||||
// Custom algorithm
|
||||
app.use(
|
||||
'/api/*',
|
||||
jwt({
|
||||
secret: 'secret',
|
||||
alg: 'HS256', // HS256 (default), HS384, HS512
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request ID
|
||||
|
||||
### requestId()
|
||||
|
||||
Generates unique request IDs.
|
||||
|
||||
```typescript
|
||||
import { requestId } from 'hono/request-id'
|
||||
|
||||
app.use('*', requestId())
|
||||
|
||||
app.get('/', (c) => {
|
||||
const id = c.get('requestId')
|
||||
return c.json({ requestId: id })
|
||||
})
|
||||
|
||||
// Custom generator
|
||||
app.use(
|
||||
'*',
|
||||
requestId({
|
||||
generator: () => `req-${Date.now()}-${Math.random()}`,
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Combine
|
||||
|
||||
### combine()
|
||||
|
||||
Combines multiple middleware into one.
|
||||
|
||||
```typescript
|
||||
import { combine } from 'hono/combine'
|
||||
import { logger } from 'hono/logger'
|
||||
import { cors } from 'hono/cors'
|
||||
import { compress } from 'hono/compress'
|
||||
|
||||
app.use('*', combine(
|
||||
logger(),
|
||||
cors(),
|
||||
compress()
|
||||
))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trailing Slash
|
||||
|
||||
### trimTrailingSlash()
|
||||
|
||||
Removes trailing slashes from URLs.
|
||||
|
||||
```typescript
|
||||
import { trimTrailingSlash } from 'hono/trailing-slash'
|
||||
|
||||
app.use('*', trimTrailingSlash())
|
||||
|
||||
// /api/users/ → /api/users
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Serve Static
|
||||
|
||||
### serveStatic()
|
||||
|
||||
Serves static files (runtime-specific).
|
||||
|
||||
**Cloudflare Workers**:
|
||||
```typescript
|
||||
import { serveStatic } from 'hono/cloudflare-workers'
|
||||
|
||||
app.use('/static/*', serveStatic({ root: './public' }))
|
||||
```
|
||||
|
||||
**Deno**:
|
||||
```typescript
|
||||
import { serveStatic } from 'hono/deno'
|
||||
|
||||
app.use('/static/*', serveStatic({ root: './public' }))
|
||||
```
|
||||
|
||||
**Bun**:
|
||||
```typescript
|
||||
import { serveStatic } from 'hono/bun'
|
||||
|
||||
app.use('/static/*', serveStatic({ root: './public' }))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Body Limit
|
||||
|
||||
### bodyLimit()
|
||||
|
||||
Limits request body size.
|
||||
|
||||
```typescript
|
||||
import { bodyLimit } from 'hono/body-limit'
|
||||
|
||||
app.use(
|
||||
'/api/*',
|
||||
bodyLimit({
|
||||
maxSize: 1024 * 1024, // 1 MB
|
||||
onError: (c) => {
|
||||
return c.json({ error: 'Request body too large' }, 413)
|
||||
},
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeout
|
||||
|
||||
### timeout()
|
||||
|
||||
Sets timeout for requests.
|
||||
|
||||
```typescript
|
||||
import { timeout } from 'hono/timeout'
|
||||
|
||||
app.use(
|
||||
'/api/*',
|
||||
timeout(5000) // 5 seconds
|
||||
)
|
||||
|
||||
// Custom error
|
||||
app.use(
|
||||
'/api/*',
|
||||
timeout(
|
||||
3000,
|
||||
(c) => c.json({ error: 'Request timeout' }, 504)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IP Restriction
|
||||
|
||||
### ipRestriction()
|
||||
|
||||
Restricts access by IP address.
|
||||
|
||||
```typescript
|
||||
import { ipRestriction } from 'hono/ip-restriction'
|
||||
|
||||
app.use(
|
||||
'/admin/*',
|
||||
ipRestriction(
|
||||
{
|
||||
allowList: ['192.168.1.0/24'],
|
||||
denyList: ['10.0.0.0/8'],
|
||||
},
|
||||
(c) => c.json({ error: 'Forbidden' }, 403)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Middleware | Purpose | Common Use Case |
|
||||
|------------|---------|-----------------|
|
||||
| `logger()` | Request logging | Development, debugging |
|
||||
| `cors()` | CORS handling | API routes |
|
||||
| `prettyJSON()` | JSON formatting | Development |
|
||||
| `compress()` | Response compression | All routes |
|
||||
| `cache()` | HTTP caching | Static assets |
|
||||
| `etag()` | ETag generation | API responses |
|
||||
| `secureHeaders()` | Security headers | All routes |
|
||||
| `timing()` | Performance metrics | Production monitoring |
|
||||
| `bearerAuth()` | Bearer token auth | API authentication |
|
||||
| `basicAuth()` | Basic auth | Admin panels |
|
||||
| `jwt()` | JWT authentication | API authentication |
|
||||
| `requestId()` | Request IDs | Logging, tracing |
|
||||
| `combine()` | Combine middleware | Clean code |
|
||||
| `trimTrailingSlash()` | URL normalization | All routes |
|
||||
| `serveStatic()` | Static files | Assets |
|
||||
| `bodyLimit()` | Body size limit | API routes |
|
||||
| `timeout()` | Request timeout | Long-running operations |
|
||||
| `ipRestriction()` | IP filtering | Admin panels |
|
||||
|
||||
---
|
||||
|
||||
**Official Documentation**: https://hono.dev/docs/guides/middleware
|
||||
542
references/rpc-guide.md
Normal file
542
references/rpc-guide.md
Normal file
@@ -0,0 +1,542 @@
|
||||
# Hono RPC Pattern Deep Dive
|
||||
|
||||
Complete guide to type-safe client/server communication using Hono's RPC feature.
|
||||
|
||||
**Last Updated**: 2025-10-22
|
||||
|
||||
---
|
||||
|
||||
## What is Hono RPC?
|
||||
|
||||
Hono RPC allows you to create **fully type-safe client/server communication** without manually defining API types. The client automatically infers types directly from server routes.
|
||||
|
||||
**Key Benefits**:
|
||||
- ✅ Full type inference (request + response)
|
||||
- ✅ No manual type definitions
|
||||
- ✅ Compile-time error checking
|
||||
- ✅ Auto-complete in IDE
|
||||
- ✅ Refactoring safety
|
||||
|
||||
---
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### Server
|
||||
|
||||
```typescript
|
||||
// server.ts
|
||||
import { Hono } from 'hono'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { z } from 'zod'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
})
|
||||
|
||||
// CRITICAL: Use const route = app.get(...) pattern
|
||||
const route = app.post(
|
||||
'/users',
|
||||
zValidator('json', userSchema),
|
||||
(c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, user: { id: '1', ...data } }, 201)
|
||||
}
|
||||
)
|
||||
|
||||
// Export type for RPC client
|
||||
export type AppType = typeof route
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```typescript
|
||||
// client.ts
|
||||
import { hc } from 'hono/client'
|
||||
import type { AppType } from './server'
|
||||
|
||||
const client = hc<AppType>('http://localhost:8787')
|
||||
|
||||
// Type-safe API call
|
||||
const res = await client.users.$post({
|
||||
json: {
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
// Type: { success: boolean, user: { id: string, name: string, email: string } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Export Patterns
|
||||
|
||||
### Pattern 1: Export Single Route
|
||||
|
||||
```typescript
|
||||
const route = app.get('/users', handler)
|
||||
export type AppType = typeof route
|
||||
```
|
||||
|
||||
**When to use**: Single route, simple API
|
||||
|
||||
### Pattern 2: Export Multiple Routes
|
||||
|
||||
```typescript
|
||||
const getUsers = app.get('/users', handler)
|
||||
const createUser = app.post('/users', handler)
|
||||
const getUser = app.get('/users/:id', handler)
|
||||
|
||||
export type AppType = typeof getUsers | typeof createUser | typeof getUser
|
||||
```
|
||||
|
||||
**When to use**: Multiple routes, moderate complexity
|
||||
|
||||
### Pattern 3: Export Sub-apps
|
||||
|
||||
```typescript
|
||||
const usersApp = new Hono()
|
||||
usersApp.get('/', handler)
|
||||
usersApp.post('/', handler)
|
||||
|
||||
export type UsersType = typeof usersApp
|
||||
```
|
||||
|
||||
**When to use**: Organized route groups, large APIs
|
||||
|
||||
### Pattern 4: Export Entire App
|
||||
|
||||
```typescript
|
||||
const app = new Hono()
|
||||
// ... many routes ...
|
||||
|
||||
export type AppType = typeof app
|
||||
```
|
||||
|
||||
**When to use**: Small apps only (performance issue with large apps)
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Problem: Slow Type Inference
|
||||
|
||||
With many routes, exporting `typeof app` causes slow IDE performance due to complex type instantiation.
|
||||
|
||||
### Solution: Export Specific Route Groups
|
||||
|
||||
```typescript
|
||||
// ❌ Slow: 100+ routes
|
||||
export type AppType = typeof app
|
||||
|
||||
// ✅ Fast: Export by domain
|
||||
const userRoutes = app.basePath('/users').get('/', ...).post('/', ...)
|
||||
const postRoutes = app.basePath('/posts').get('/', ...).post('/', ...)
|
||||
|
||||
export type UserRoutes = typeof userRoutes
|
||||
export type PostRoutes = typeof postRoutes
|
||||
|
||||
// Client uses specific routes
|
||||
const userClient = hc<UserRoutes>('http://localhost:8787/users')
|
||||
const postClient = hc<PostRoutes>('http://localhost:8787/posts')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Usage Patterns
|
||||
|
||||
### Basic GET Request
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$get()
|
||||
const data = await res.json()
|
||||
```
|
||||
|
||||
### POST with JSON Body
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$post({
|
||||
json: {
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
```
|
||||
|
||||
### Route Parameters
|
||||
|
||||
```typescript
|
||||
const res = await client.users[':id'].$get({
|
||||
param: { id: '123' },
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
```typescript
|
||||
const res = await client.search.$get({
|
||||
query: {
|
||||
q: 'hello',
|
||||
page: '2',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
```
|
||||
|
||||
### Custom Headers
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$get({}, {
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Fetch Options
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$get({}, {
|
||||
signal: AbortSignal.timeout(5000), // 5 second timeout
|
||||
cache: 'no-cache',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$post({
|
||||
json: { name: 'Alice', email: 'alice@example.com' },
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('Request failed:', res.status)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
```
|
||||
|
||||
### Typed Error Responses
|
||||
|
||||
```typescript
|
||||
const res = await client.users.$post({
|
||||
json: { name: '', email: 'invalid' },
|
||||
})
|
||||
|
||||
if (res.status === 400) {
|
||||
const error = await res.json() // Typed as error response
|
||||
console.error('Validation error:', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (res.status === 500) {
|
||||
const error = await res.json()
|
||||
console.error('Server error:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await res.json() // Typed as success response
|
||||
```
|
||||
|
||||
### Try-Catch
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const res = await client.users.$get()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
console.log('Users:', data)
|
||||
} catch (error) {
|
||||
console.error('Network error:', error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Bearer Token
|
||||
|
||||
```typescript
|
||||
const client = hc<AppType>('http://localhost:8787', {
|
||||
headers: {
|
||||
Authorization: 'Bearer your-token-here',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await client.protected.$get()
|
||||
```
|
||||
|
||||
### Dynamic Headers
|
||||
|
||||
```typescript
|
||||
async function makeAuthenticatedRequest() {
|
||||
const token = await getAuthToken()
|
||||
|
||||
const res = await client.protected.$get({}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
return res.json()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React Integration
|
||||
|
||||
### Basic Hook
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react'
|
||||
import { hc } from 'hono/client'
|
||||
import type { AppType } from './server'
|
||||
|
||||
const client = hc<AppType>('http://localhost:8787')
|
||||
|
||||
function useUsers() {
|
||||
const [users, setUsers] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUsers() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const res = await client.users.$get()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch users')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setUsers(data.users)
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
return { users, loading, error }
|
||||
}
|
||||
```
|
||||
|
||||
### With TanStack Query
|
||||
|
||||
```typescript
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { hc } from 'hono/client'
|
||||
import type { AppType } from './server'
|
||||
|
||||
const client = hc<AppType>('http://localhost:8787')
|
||||
|
||||
function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
const res = await client.users.$get()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch users')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function UsersComponent() {
|
||||
const { data, isLoading, error } = useUsers()
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data?.users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Middleware Responses
|
||||
|
||||
Server:
|
||||
```typescript
|
||||
const authMiddleware = async (c, next) => {
|
||||
const token = c.req.header('Authorization')
|
||||
|
||||
if (!token) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
||||
|
||||
const route = app.get('/protected', authMiddleware, (c) => {
|
||||
return c.json({ data: 'Protected data' })
|
||||
})
|
||||
|
||||
export type ProtectedType = typeof route
|
||||
```
|
||||
|
||||
Client:
|
||||
```typescript
|
||||
const res = await client.protected.$get()
|
||||
|
||||
// Response type includes both middleware (401) and handler (200) responses
|
||||
if (res.status === 401) {
|
||||
const error = await res.json() // Type: { error: string }
|
||||
console.error('Unauthorized:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await res.json() // Type: { data: string }
|
||||
```
|
||||
|
||||
### Multiple Sub-apps
|
||||
|
||||
Server:
|
||||
```typescript
|
||||
const usersApp = new Hono()
|
||||
usersApp.get('/', handler)
|
||||
usersApp.post('/', handler)
|
||||
|
||||
const postsApp = new Hono()
|
||||
postsApp.get('/', handler)
|
||||
postsApp.post('/', handler)
|
||||
|
||||
const app = new Hono()
|
||||
app.route('/users', usersApp)
|
||||
app.route('/posts', postsApp)
|
||||
|
||||
export type UsersType = typeof usersApp
|
||||
export type PostsType = typeof postsApp
|
||||
```
|
||||
|
||||
Client:
|
||||
```typescript
|
||||
const userClient = hc<UsersType>('http://localhost:8787/users')
|
||||
const postClient = hc<PostsType>('http://localhost:8787/posts')
|
||||
|
||||
const users = await userClient.index.$get()
|
||||
const posts = await postClient.index.$get()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Tips
|
||||
|
||||
### Type Inference
|
||||
|
||||
```typescript
|
||||
// Infer request type
|
||||
type UserRequest = Parameters<typeof client.users.$post>[0]['json']
|
||||
// Type: { name: string, email: string }
|
||||
|
||||
// Infer response type
|
||||
type UserResponse = Awaited<ReturnType<typeof client.users.$post>>
|
||||
type UserData = Awaited<ReturnType<UserResponse['json']>>
|
||||
// Type: { success: boolean, user: { id: string, name: string, email: string } }
|
||||
```
|
||||
|
||||
### Generic Client Functions
|
||||
|
||||
```typescript
|
||||
async function fetchFromAPI<T extends typeof client[keyof typeof client]>(
|
||||
endpoint: T,
|
||||
options?: Parameters<T['$get']>[0]
|
||||
) {
|
||||
const res = await endpoint.$get(options)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Usage
|
||||
const users = await fetchFromAPI(client.users)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
1. **Export specific routes** for large APIs
|
||||
2. **Use route groups** for better organization
|
||||
3. **Batch requests** when possible
|
||||
4. **Cache client instance** (don't recreate on every request)
|
||||
5. **Use AbortController** for request cancellation
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Don't: Anonymous Routes
|
||||
|
||||
```typescript
|
||||
app.get('/users', (c) => c.json({ users: [] }))
|
||||
export type AppType = typeof app // Won't infer route properly
|
||||
```
|
||||
|
||||
### ✅ Do: Named Routes
|
||||
|
||||
```typescript
|
||||
const route = app.get('/users', (c) => c.json({ users: [] }))
|
||||
export type AppType = typeof route
|
||||
```
|
||||
|
||||
### ❌ Don't: Forget Type Import
|
||||
|
||||
```typescript
|
||||
import { AppType } from './server' // Wrong: runtime import
|
||||
```
|
||||
|
||||
### ✅ Do: Type-Only Import
|
||||
|
||||
```typescript
|
||||
import type { AppType } from './server' // Correct: type-only import
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Official Documentation
|
||||
|
||||
- **Hono RPC Guide**: https://hono.dev/docs/guides/rpc
|
||||
- **hc Client API**: https://hono.dev/docs/helpers/hc
|
||||
451
references/top-errors.md
Normal file
451
references/top-errors.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# Common Hono Errors and Solutions
|
||||
|
||||
Complete troubleshooting guide for Hono routing and middleware errors.
|
||||
|
||||
**Last Updated**: 2025-10-22
|
||||
|
||||
---
|
||||
|
||||
## Error #1: Middleware Response Not Typed in RPC
|
||||
|
||||
**Error Message**: Client doesn't infer middleware response types
|
||||
|
||||
**Cause**: RPC mode doesn't automatically infer middleware responses by default
|
||||
|
||||
**Source**: [honojs/hono#2719](https://github.com/honojs/hono/issues/2719)
|
||||
|
||||
**Solution**: Export specific route types that include middleware
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Client doesn't see middleware response
|
||||
const route = app.get('/data', authMiddleware, handler)
|
||||
export type AppType = typeof app
|
||||
|
||||
// ✅ Correct: Export route directly
|
||||
const route = app.get('/data', authMiddleware, handler)
|
||||
export type AppType = typeof route
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #2: RPC Type Inference Slow
|
||||
|
||||
**Error Message**: IDE becomes slow or unresponsive with many routes
|
||||
|
||||
**Cause**: Complex type instantiation from `typeof app` with large number of routes
|
||||
|
||||
**Source**: [hono.dev/docs/guides/rpc](https://hono.dev/docs/guides/rpc)
|
||||
|
||||
**Solution**: Export specific route groups instead of entire app
|
||||
|
||||
```typescript
|
||||
// ❌ Slow: Export entire app
|
||||
export type AppType = typeof app
|
||||
|
||||
// ✅ Fast: Export specific routes
|
||||
const userRoutes = app.get('/users', ...).post('/users', ...)
|
||||
export type UserRoutes = typeof userRoutes
|
||||
|
||||
const postRoutes = app.get('/posts', ...).post('/posts', ...)
|
||||
export type PostRoutes = typeof postRoutes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #3: Middleware Chain Broken
|
||||
|
||||
**Error Message**: Handler not executed, middleware returns early
|
||||
|
||||
**Cause**: Forgot to call `await next()` in middleware
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Always call `await next()` unless intentionally short-circuiting
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Forgot await next()
|
||||
app.use('*', async (c, next) => {
|
||||
console.log('Before')
|
||||
// Missing: await next()
|
||||
console.log('After')
|
||||
})
|
||||
|
||||
// ✅ Correct: Call await next()
|
||||
app.use('*', async (c, next) => {
|
||||
console.log('Before')
|
||||
await next()
|
||||
console.log('After')
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #4: Validation Error Not Handled
|
||||
|
||||
**Error Message**: Validation fails silently or returns wrong status code
|
||||
|
||||
**Cause**: No custom error handler for validation failures
|
||||
|
||||
**Source**: Best practices
|
||||
|
||||
**Solution**: Use custom validation hooks
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Default 400 response with no details
|
||||
app.post('/users', zValidator('json', schema), handler)
|
||||
|
||||
// ✅ Correct: Custom error handler
|
||||
app.post(
|
||||
'/users',
|
||||
zValidator('json', schema, (result, c) => {
|
||||
if (!result.success) {
|
||||
return c.json({ error: 'Validation failed', issues: result.error.issues }, 400)
|
||||
}
|
||||
}),
|
||||
handler
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #5: Context Type Safety Lost
|
||||
|
||||
**Error Message**: `c.get()` returns `any` type
|
||||
|
||||
**Cause**: Not defining `Variables` type in Hono generic
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Define Variables type
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: No Variables type
|
||||
const app = new Hono()
|
||||
app.use('*', (c, next) => {
|
||||
c.set('user', { id: 1 }) // No type checking
|
||||
await next()
|
||||
})
|
||||
|
||||
// ✅ Correct: Define Variables type
|
||||
type Variables = {
|
||||
user: { id: number; name: string }
|
||||
}
|
||||
|
||||
const app = new Hono<{ Variables: Variables }>()
|
||||
app.use('*', (c, next) => {
|
||||
c.set('user', { id: 1, name: 'Alice' }) // Type-safe!
|
||||
await next()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #6: Route Parameter Type Error
|
||||
|
||||
**Error Message**: `c.req.param()` returns string but number expected
|
||||
|
||||
**Cause**: Route parameters are always strings
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Use validation to transform to correct type
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Assuming number type
|
||||
app.get('/users/:id', (c) => {
|
||||
const id = c.req.param('id') // Type: string
|
||||
const user = await db.findUser(id) // Error: expects number
|
||||
return c.json({ user })
|
||||
})
|
||||
|
||||
// ✅ Correct: Validate and transform
|
||||
const idSchema = z.object({
|
||||
id: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
|
||||
})
|
||||
|
||||
app.get('/users/:id', zValidator('param', idSchema), async (c) => {
|
||||
const { id } = c.req.valid('param') // Type: number
|
||||
const user = await db.findUser(id)
|
||||
return c.json({ user })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #7: Missing Error Check After Middleware
|
||||
|
||||
**Error Message**: Errors in handlers not caught
|
||||
|
||||
**Cause**: Not checking `c.error` after `await next()`
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Check `c.error` in middleware
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: No error checking
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
// Missing error check
|
||||
})
|
||||
|
||||
// ✅ Correct: Check c.error
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
|
||||
if (c.error) {
|
||||
console.error('Error:', c.error)
|
||||
// Send to error tracking service
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #8: HTTPException Misuse
|
||||
|
||||
**Error Message**: Errors not handled correctly
|
||||
|
||||
**Cause**: Throwing plain Error instead of HTTPException
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Use HTTPException for client errors
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Plain Error
|
||||
app.get('/users/:id', (c) => {
|
||||
if (!id) {
|
||||
throw new Error('ID is required') // No status code
|
||||
}
|
||||
})
|
||||
|
||||
// ✅ Correct: HTTPException
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
app.get('/users/:id', (c) => {
|
||||
if (!id) {
|
||||
throw new HTTPException(400, { message: 'ID is required' })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #9: Query Parameter Not Validated
|
||||
|
||||
**Error Message**: Invalid query parameters cause errors
|
||||
|
||||
**Cause**: Accessing `c.req.query()` without validation
|
||||
|
||||
**Source**: Best practices
|
||||
|
||||
**Solution**: Validate query parameters
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: No validation
|
||||
app.get('/search', (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1', 10) // May be NaN
|
||||
const limit = parseInt(c.req.query('limit') || '10', 10)
|
||||
// ...
|
||||
})
|
||||
|
||||
// ✅ Correct: Validate query params
|
||||
const querySchema = z.object({
|
||||
page: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1)),
|
||||
limit: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(100)),
|
||||
})
|
||||
|
||||
app.get('/search', zValidator('query', querySchema), (c) => {
|
||||
const { page, limit } = c.req.valid('query') // Type-safe!
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #10: Incorrect Middleware Order
|
||||
|
||||
**Error Message**: Middleware executing in wrong order
|
||||
|
||||
**Cause**: Misunderstanding middleware execution flow
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Remember middleware runs top-to-bottom before handler, bottom-to-top after
|
||||
|
||||
```typescript
|
||||
// Middleware execution order:
|
||||
app.use('*', async (c, next) => {
|
||||
console.log('1: Before') // Runs 1st
|
||||
await next()
|
||||
console.log('4: After') // Runs 4th
|
||||
})
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
console.log('2: Before') // Runs 2nd
|
||||
await next()
|
||||
console.log('3: After') // Runs 3rd
|
||||
})
|
||||
|
||||
app.get('/', (c) => {
|
||||
console.log('Handler') // Runs in between
|
||||
return c.json({})
|
||||
})
|
||||
|
||||
// Output: 1, 2, Handler, 3, 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #11: JSON Parsing Error
|
||||
|
||||
**Error Message**: `SyntaxError: Unexpected token in JSON`
|
||||
|
||||
**Cause**: Request body is not valid JSON
|
||||
|
||||
**Source**: Common issue
|
||||
|
||||
**Solution**: Add validation and error handling
|
||||
|
||||
```typescript
|
||||
app.post('/data', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json()
|
||||
return c.json({ success: true, body })
|
||||
} catch (error) {
|
||||
return c.json({ error: 'Invalid JSON' }, 400)
|
||||
}
|
||||
})
|
||||
|
||||
// Or use validator (handles automatically)
|
||||
app.post('/data', zValidator('json', schema), (c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #12: CORS Preflight Fails
|
||||
|
||||
**Error Message**: CORS preflight request fails
|
||||
|
||||
**Cause**: Missing CORS middleware or incorrect configuration
|
||||
|
||||
**Source**: Common issue
|
||||
|
||||
**Solution**: Configure CORS middleware correctly
|
||||
|
||||
```typescript
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
app.use(
|
||||
'/api/*',
|
||||
cors({
|
||||
origin: ['https://example.com'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #13: Route Not Found
|
||||
|
||||
**Error Message**: 404 Not Found for existing route
|
||||
|
||||
**Cause**: Route pattern doesn't match request path
|
||||
|
||||
**Source**: Common issue
|
||||
|
||||
**Solution**: Check route pattern and parameter syntax
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Missing colon for parameter
|
||||
app.get('/users/id', handler) // Matches "/users/id" literally
|
||||
|
||||
// ✅ Correct: Parameter with colon
|
||||
app.get('/users/:id', handler) // Matches "/users/123"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #14: Response Already Sent
|
||||
|
||||
**Error Message**: Cannot set headers after response sent
|
||||
|
||||
**Cause**: Trying to modify response after calling `c.json()` or `c.text()`
|
||||
|
||||
**Source**: Common issue
|
||||
|
||||
**Solution**: Return response immediately, don't modify after
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Trying to modify after response
|
||||
app.get('/data', (c) => {
|
||||
const response = c.json({ data: 'value' })
|
||||
c.res.headers.set('X-Custom', 'value') // Error!
|
||||
return response
|
||||
})
|
||||
|
||||
// ✅ Correct: Set headers before response
|
||||
app.get('/data', (c) => {
|
||||
c.res.headers.set('X-Custom', 'value')
|
||||
return c.json({ data: 'value' })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #15: Type Inference Not Working
|
||||
|
||||
**Error Message**: TypeScript not inferring types from validator
|
||||
|
||||
**Cause**: Not using `c.req.valid()` after validation
|
||||
|
||||
**Source**: Official docs
|
||||
|
||||
**Solution**: Always use `c.req.valid()` for type-safe access
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: No type inference
|
||||
app.post('/users', zValidator('json', schema), async (c) => {
|
||||
const body = await c.req.json() // Type: any
|
||||
return c.json({ body })
|
||||
})
|
||||
|
||||
// ✅ Correct: Type-safe
|
||||
app.post('/users', zValidator('json', schema), (c) => {
|
||||
const data = c.req.valid('json') // Type: inferred from schema
|
||||
return c.json({ data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Middleware response not typed | RPC doesn't infer middleware | Export route, not app |
|
||||
| Slow RPC type inference | Too many routes | Export specific route groups |
|
||||
| Middleware chain broken | Missing `await next()` | Always call `await next()` |
|
||||
| Validation error unhandled | No custom hook | Use custom validation hook |
|
||||
| Context type safety lost | No Variables type | Define Variables type |
|
||||
| Route param type error | Params are strings | Use validation to transform |
|
||||
| Missing error check | Not checking `c.error` | Check `c.error` after `next()` |
|
||||
| HTTPException misuse | Using plain Error | Use HTTPException |
|
||||
| Query param not validated | Direct access | Use query validator |
|
||||
| Incorrect middleware order | Misunderstanding flow | Review execution order |
|
||||
| JSON parsing error | Invalid JSON | Add error handling |
|
||||
| CORS preflight fails | Missing CORS config | Configure CORS middleware |
|
||||
| Route not found | Wrong pattern | Check route syntax |
|
||||
| Response already sent | Modifying after send | Set headers before response |
|
||||
| Type inference not working | Not using `c.req.valid()` | Use `c.req.valid()` |
|
||||
|
||||
---
|
||||
|
||||
**Official Documentation**: https://hono.dev/docs
|
||||
313
references/validation-libraries.md
Normal file
313
references/validation-libraries.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# Validation Libraries Comparison
|
||||
|
||||
Comprehensive comparison of validation libraries for Hono: Zod, Valibot, Typia, and ArkType.
|
||||
|
||||
**Last Updated**: 2025-10-22
|
||||
|
||||
---
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
| Feature | Zod | Valibot | Typia | ArkType |
|
||||
|---------|-----|---------|-------|---------|
|
||||
| **Bundle Size** | ~57KB | ~1-5KB | ~0KB | ~15KB |
|
||||
| **Performance** | Good | Excellent | Best | Excellent |
|
||||
| **Type Safety** | Excellent | Excellent | Best | Excellent |
|
||||
| **Ecosystem** | Largest | Growing | Small | Growing |
|
||||
| **Learning Curve** | Easy | Easy | Medium | Easy |
|
||||
| **Compilation** | Runtime | Runtime | AOT | Runtime |
|
||||
| **Tree Shaking** | Limited | Excellent | N/A | Good |
|
||||
|
||||
---
|
||||
|
||||
## Zod
|
||||
|
||||
**Install**: `npm install zod @hono/zod-validator`
|
||||
|
||||
**Pros**:
|
||||
- ✅ Most popular (11M+ weekly downloads)
|
||||
- ✅ Extensive ecosystem and community
|
||||
- ✅ Excellent TypeScript support
|
||||
- ✅ Rich feature set (transforms, refinements, etc.)
|
||||
- ✅ Great documentation
|
||||
|
||||
**Cons**:
|
||||
- ❌ Larger bundle size (~57KB)
|
||||
- ❌ Slower performance vs alternatives
|
||||
- ❌ Limited tree-shaking
|
||||
|
||||
**Best for**:
|
||||
- Production applications with complex validation needs
|
||||
- Projects prioritizing ecosystem and community support
|
||||
- Teams familiar with Zod
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(18).optional(),
|
||||
})
|
||||
|
||||
app.post('/users', zValidator('json', schema), (c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Valibot
|
||||
|
||||
**Install**: `npm install valibot @hono/valibot-validator`
|
||||
|
||||
**Pros**:
|
||||
- ✅ Tiny bundle size (1-5KB with tree-shaking)
|
||||
- ✅ Excellent performance
|
||||
- ✅ Modular design (import only what you need)
|
||||
- ✅ Similar API to Zod
|
||||
- ✅ Great TypeScript support
|
||||
|
||||
**Cons**:
|
||||
- ❌ Smaller ecosystem vs Zod
|
||||
- ❌ Newer library (less battle-tested)
|
||||
- ❌ Fewer integrations
|
||||
|
||||
**Best for**:
|
||||
- Applications prioritizing bundle size
|
||||
- Performance-critical applications
|
||||
- Projects that want Zod-like API with better performance
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { vValidator } from '@hono/valibot-validator'
|
||||
import * as v from 'valibot'
|
||||
|
||||
const schema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
|
||||
email: v.pipe(v.string(), v.email()),
|
||||
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(18))),
|
||||
})
|
||||
|
||||
app.post('/users', vValidator('json', schema), (c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typia
|
||||
|
||||
**Install**: `npm install typia @hono/typia-validator`
|
||||
|
||||
**Pros**:
|
||||
- ✅ **Fastest** validation (AOT compilation)
|
||||
- ✅ Zero runtime overhead
|
||||
- ✅ No bundle size impact
|
||||
- ✅ Uses TypeScript types directly
|
||||
- ✅ Compile-time validation
|
||||
|
||||
**Cons**:
|
||||
- ❌ Requires build step (TypeScript transformer)
|
||||
- ❌ More complex setup
|
||||
- ❌ Smaller community
|
||||
- ❌ Limited to TypeScript
|
||||
|
||||
**Best for**:
|
||||
- Maximum performance requirements
|
||||
- Applications with strict bundle size constraints
|
||||
- Projects already using TypeScript transformers
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { typiaValidator } from '@hono/typia-validator'
|
||||
import typia from 'typia'
|
||||
|
||||
interface User {
|
||||
name: string
|
||||
email: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
const validate = typia.createValidate<User>()
|
||||
|
||||
app.post('/users', typiaValidator('json', validate), (c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ArkType
|
||||
|
||||
**Install**: `npm install arktype @hono/arktype-validator`
|
||||
|
||||
**Pros**:
|
||||
- ✅ Excellent performance (1.5x faster than Zod)
|
||||
- ✅ Intuitive string-based syntax
|
||||
- ✅ Great error messages
|
||||
- ✅ TypeScript-first
|
||||
- ✅ Small bundle size (~15KB)
|
||||
|
||||
**Cons**:
|
||||
- ❌ Newer library
|
||||
- ❌ Smaller ecosystem
|
||||
- ❌ Different syntax (learning curve)
|
||||
|
||||
**Best for**:
|
||||
- Developers who prefer string-based schemas
|
||||
- Performance-conscious projects
|
||||
- Projects that value developer experience
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
import { arktypeValidator } from '@hono/arktype-validator'
|
||||
import { type } from 'arktype'
|
||||
|
||||
const schema = type({
|
||||
name: 'string',
|
||||
email: 'email',
|
||||
'age?': 'number>=18',
|
||||
})
|
||||
|
||||
app.post('/users', arktypeValidator('json', schema), (c) => {
|
||||
const data = c.req.valid('json')
|
||||
return c.json({ success: true, data })
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Based on community benchmarks (approximate):
|
||||
|
||||
```
|
||||
Typia: ~10,000,000 validations/sec (Fastest - AOT)
|
||||
Valibot: ~5,000,000 validations/sec
|
||||
ArkType: ~3,500,000 validations/sec
|
||||
Zod: ~2,300,000 validations/sec
|
||||
```
|
||||
|
||||
**Note**: Actual performance varies by schema complexity and runtime.
|
||||
|
||||
---
|
||||
|
||||
## Bundle Size Comparison
|
||||
|
||||
```
|
||||
Typia: 0 KB (AOT compilation)
|
||||
Valibot: 1-5 KB (with tree-shaking)
|
||||
ArkType: ~15 KB
|
||||
Zod: ~57 KB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Comparison
|
||||
|
||||
### Transformations
|
||||
|
||||
| Library | Support | Example |
|
||||
|---------|---------|---------|
|
||||
| Zod | ✅ | `z.string().transform(Number)` |
|
||||
| Valibot | ✅ | `v.pipe(v.string(), v.transform(Number))` |
|
||||
| Typia | ✅ | Built into types |
|
||||
| ArkType | ✅ | Type inference |
|
||||
|
||||
### Refinements
|
||||
|
||||
| Library | Support | Example |
|
||||
|---------|---------|---------|
|
||||
| Zod | ✅ | `z.string().refine((val) => val.length > 0)` |
|
||||
| Valibot | ✅ | `v.pipe(v.string(), v.check((val) => val.length > 0))` |
|
||||
| Typia | ✅ | Custom validators |
|
||||
| ArkType | ✅ | Narrow types |
|
||||
|
||||
### Default Values
|
||||
|
||||
| Library | Support | Example |
|
||||
|---------|---------|---------|
|
||||
| Zod | ✅ | `z.string().default('default')` |
|
||||
| Valibot | ✅ | `v.optional(v.string(), 'default')` |
|
||||
| Typia | ⚠️ | Limited |
|
||||
| ArkType | ✅ | Type defaults |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Choose **Zod** if:
|
||||
- You want the largest ecosystem and community
|
||||
- You need extensive documentation and examples
|
||||
- Bundle size is not a primary concern
|
||||
- You want battle-tested reliability
|
||||
|
||||
### Choose **Valibot** if:
|
||||
- Bundle size is critical
|
||||
- You want Zod-like API with better performance
|
||||
- You're building a modern application with tree-shaking
|
||||
- You want modular imports
|
||||
|
||||
### Choose **Typia** if:
|
||||
- Performance is absolutely critical
|
||||
- You can afford a more complex build setup
|
||||
- Zero runtime overhead is required
|
||||
- You're already using TypeScript transformers
|
||||
|
||||
### Choose **ArkType** if:
|
||||
- You prefer string-based schema syntax
|
||||
- You want excellent error messages
|
||||
- Performance is important but not critical
|
||||
- You value developer experience
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Zod → Valibot
|
||||
|
||||
```typescript
|
||||
// Zod
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
age: z.number().optional(),
|
||||
})
|
||||
|
||||
// Valibot
|
||||
const schema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(1)),
|
||||
age: v.optional(v.number()),
|
||||
})
|
||||
```
|
||||
|
||||
### Zod → ArkType
|
||||
|
||||
```typescript
|
||||
// Zod
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
age: z.number().optional(),
|
||||
})
|
||||
|
||||
// ArkType
|
||||
const schema = type({
|
||||
name: 'string>0',
|
||||
'age?': 'number',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Official Documentation
|
||||
|
||||
- **Zod**: https://zod.dev
|
||||
- **Valibot**: https://valibot.dev
|
||||
- **Typia**: https://typia.io
|
||||
- **ArkType**: https://arktype.io
|
||||
- **Hono Validators**: https://hono.dev/docs/guides/validation
|
||||
74
scripts/check-versions.sh
Executable file
74
scripts/check-versions.sh
Executable 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)"
|
||||
422
templates/context-extension.ts
Normal file
422
templates/context-extension.ts
Normal 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
409
templates/error-handling.ts
Normal 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,
|
||||
}
|
||||
418
templates/middleware-composition.ts
Normal file
418
templates/middleware-composition.ts
Normal 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
33
templates/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
299
templates/routing-patterns.ts
Normal file
299
templates/routing-patterns.ts
Normal 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
268
templates/rpc-client.ts
Normal 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
202
templates/rpc-pattern.ts
Normal 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
|
||||
315
templates/validation-valibot.ts
Normal file
315
templates/validation-valibot.ts
Normal 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
428
templates/validation-zod.ts
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user