Initial commit
This commit is contained in:
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 }
|
||||
Reference in New Issue
Block a user