Initial commit

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

View File

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

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

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

View File

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

33
templates/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "hono-app",
"version": "1.0.0",
"type": "module",
"description": "Hono application with routing, middleware, validation, and RPC",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"type-check": "tsc --noEmit"
},
"dependencies": {
"hono": "^4.10.2"
},
"devDependencies": {
"typescript": "^5.9.0",
"tsx": "^4.19.0",
"@types/node": "^22.10.0"
},
"optionalDependencies": {
"zod": "^4.1.12",
"valibot": "^1.1.0",
"@hono/zod-validator": "^0.7.4",
"@hono/valibot-validator": "^0.5.3",
"@hono/typia-validator": "^0.1.2",
"@hono/arktype-validator": "^2.0.1",
"arktype": "^2.0.0",
"typia": "^7.1.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

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

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

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

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

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

View File

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

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

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