Initial commit
This commit is contained in:
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