playbook/outfitter-agents/plugins/outfitter/skills/hono-dev/references/middleware.md

458 lines
9.2 KiB
Markdown

# Middleware Patterns
Common middleware patterns for Hono APIs.
## Built-in Middleware
### Logger
```typescript
import { logger } from 'hono/logger';
app.use('*', logger());
// Custom log function
app.use('*', logger((message) => {
console.log(`[${new Date().toISOString()}] ${message}`);
}));
```
### CORS
```typescript
import { cors } from 'hono/cors';
// Basic CORS
app.use('/api/*', cors());
// Configured CORS
app.use('/api/*', cors({
origin: ['http://localhost:3000', 'https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400, // 24 hours
}));
// Dynamic origin
app.use('/api/*', cors({
origin: (origin) => {
if (origin.endsWith('.example.com')) {
return origin;
}
return null;
},
}));
```
### Compress
```typescript
import { compress } from 'hono/compress';
app.use('*', compress());
```
### Secure Headers
```typescript
import { secureHeaders } from 'hono/secure-headers';
app.use('*', secureHeaders());
```
### Bearer Auth
```typescript
import { bearerAuth } from 'hono/bearer-auth';
app.use('/api/*', bearerAuth({
token: Bun.env.API_TOKEN!,
}));
// Multiple tokens
app.use('/api/*', bearerAuth({
token: [Bun.env.API_TOKEN!, Bun.env.ADMIN_TOKEN!],
}));
// Custom verification
app.use('/api/*', bearerAuth({
verifyToken: async (token, c) => {
const user = await verifyJWT(token);
if (user) {
c.set('user', user);
return true;
}
return false;
},
}));
```
### Basic Auth
```typescript
import { basicAuth } from 'hono/basic-auth';
app.use('/admin/*', basicAuth({
username: 'admin',
password: Bun.env.ADMIN_PASSWORD!,
}));
```
## Custom Middleware with Factory
### Authentication
```typescript
import { createFactory } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
type Env = {
Variables: {
user: { id: string; email: string; role: 'admin' | 'user' };
};
};
const factory = createFactory<Env>();
export const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HTTPException(401, { message: 'Missing authorization token' });
}
const payload = await verifyJWT(token);
if (!payload) {
throw new HTTPException(401, { message: 'Invalid token' });
}
c.set('user', {
id: payload.sub,
email: payload.email,
role: payload.role,
});
await next();
});
```
### Optional Authentication
```typescript
export const optionalAuth = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (token) {
try {
const payload = await verifyJWT(token);
if (payload) {
c.set('user', {
id: payload.sub,
email: payload.email,
role: payload.role,
});
}
} catch {
// Invalid token, continue without user
}
}
await next();
});
```
### Role-Based Access Control
```typescript
export const requireRole = (requiredRole: 'admin' | 'user') => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin has access to everything
if (user.role === 'admin') {
await next();
return;
}
if (user.role !== requiredRole) {
throw new HTTPException(403, { message: `${requiredRole} access required` });
}
await next();
});
};
// Usage
app.use('/api/admin/*', requireRole('admin'));
```
### Resource Ownership
```typescript
export const requireOwnership = (paramName = 'userId') => {
return factory.createMiddleware(async (c, next) => {
const user = c.get('user');
if (!user) {
throw new HTTPException(401, { message: 'Unauthorized' });
}
// Admin bypasses ownership check
if (user.role === 'admin') {
await next();
return;
}
const resourceUserId = c.req.param(paramName);
if (user.id !== resourceUserId) {
throw new HTTPException(403, { message: 'Access denied' });
}
await next();
});
};
// Usage
app.delete('/users/:userId', requireOwnership('userId'), deleteUser);
```
### Request ID
```typescript
export const requestIdMiddleware = factory.createMiddleware(async (c, next) => {
const requestId = c.req.header('x-request-id') || crypto.randomUUID();
c.set('requestId', requestId);
await next();
c.res.headers.set('x-request-id', requestId);
});
```
### Request Timing
```typescript
export const timingMiddleware = factory.createMiddleware(async (c, next) => {
const start = Bun.nanoseconds();
await next();
const duration = (Bun.nanoseconds() - start) / 1_000_000;
c.res.headers.set('x-response-time', `${duration.toFixed(2)}ms`);
console.log(`${c.req.method} ${c.req.path} - ${duration.toFixed(2)}ms`);
});
```
### Rate Limiting
```typescript
const rateLimits = new Map<string, { count: number; resetAt: number }>();
export const rateLimiter = (limit: number, windowMs: number) => {
return factory.createMiddleware(async (c, next) => {
const ip = c.req.header('x-forwarded-for') || 'unknown';
const now = Date.now();
const entry = rateLimits.get(ip);
if (!entry || now > entry.resetAt) {
rateLimits.set(ip, { count: 1, resetAt: now + windowMs });
} else {
entry.count++;
if (entry.count > limit) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
throw new HTTPException(429, {
message: 'Rate limit exceeded',
cause: { retryAfter },
});
}
}
await next();
});
};
// Usage: 100 requests per minute
app.use('/api/*', rateLimiter(100, 60 * 1000));
```
### Database Connection
```typescript
import { Database } from 'bun:sqlite';
export const dbMiddleware = factory.createMiddleware(async (c, next) => {
const db = new Database('app.db');
c.set('db', db);
try {
await next();
} finally {
db.close();
}
});
// With connection pooling
class DatabasePool {
private pool: Database[] = [];
get(): Database {
return this.pool.pop() || new Database('app.db');
}
release(db: Database) {
this.pool.push(db);
}
}
const pool = new DatabasePool();
export const pooledDbMiddleware = factory.createMiddleware(async (c, next) => {
const db = pool.get();
c.set('db', db);
try {
await next();
} finally {
pool.release(db);
}
});
```
### Caching
```typescript
const cache = new Map<string, { data: any; expiresAt: number }>();
export const cacheMiddleware = (ttlMs: number) => {
return factory.createMiddleware(async (c, next) => {
if (c.req.method !== 'GET') {
await next();
return;
}
const key = c.req.url;
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
return c.json(cached.data);
}
await next();
// Cache response after handler
const response = c.res.clone();
const data = await response.json();
cache.set(key, {
data,
expiresAt: Date.now() + ttlMs,
});
});
};
// Usage: 5 minute cache
app.get('/api/public-data', cacheMiddleware(5 * 60 * 1000), handler);
```
### Request Validation
```typescript
import { z } from 'zod';
export const validateRequest = <T extends z.ZodType>(schema: T) => {
return factory.createMiddleware(async (c, next) => {
try {
const body = await c.req.json();
schema.parse(body);
} catch (err) {
if (err instanceof z.ZodError) {
throw new HTTPException(400, {
message: 'Validation failed',
cause: err.issues,
});
}
throw err;
}
await next();
});
};
```
## Middleware Composition
```typescript
// Compose multiple middleware
const apiMiddleware = factory.createMiddleware(async (c, next) => {
// Request ID
c.set('requestId', crypto.randomUUID());
// Timing start
const start = Bun.nanoseconds();
await next();
// Timing end
const duration = (Bun.nanoseconds() - start) / 1_000_000;
c.res.headers.set('x-request-id', c.get('requestId'));
c.res.headers.set('x-response-time', `${duration.toFixed(2)}ms`);
});
// Apply composed middleware
app.use('/api/*', apiMiddleware);
```
## Conditional Middleware
```typescript
export const conditionalAuth = (condition: (c: Context) => boolean) => {
return factory.createMiddleware(async (c, next) => {
if (condition(c)) {
await authMiddleware(c, next);
} else {
await next();
}
});
};
// Skip auth for health checks
app.use('/api/*', conditionalAuth((c) => c.req.path !== '/api/health'));
```
## Middleware Order
```typescript
const app = factory.createApp()
// Global middleware (runs for all routes)
.use('*', logger())
.use('*', requestIdMiddleware)
.use('*', timingMiddleware)
// API middleware
.use('/api/*', cors())
.use('/api/*', dbMiddleware)
// Public routes (before auth middleware)
.get('/api/health', (c) => c.json({ status: 'ok' }))
.post('/api/auth/login', loginHandler)
// Protected routes
.use('/api/*', authMiddleware)
.get('/api/profile', profileHandler)
.get('/api/users', usersHandler)
// Admin routes
.use('/api/admin/*', requireRole('admin'))
.get('/api/admin/stats', statsHandler);
```