Middleware
Superfunction provides built-in middleware for common patterns and supports custom middleware creation using defineMiddleware.
defineMiddleware
Create named middleware that can be referenced in .skip() calls.
typescriptimport { defineMiddleware } from '@spfn/core/route'; export const authMiddleware = defineMiddleware('auth', async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', ''); if (!token) { throw new UnauthorizedError({ message: 'No token provided' }); } const user = await verifyToken(token); c.set('user', user); await next(); });
Signature
typescriptfunction defineMiddleware<TName extends string>( name: TName, handler: MiddlewareHandler ): NamedMiddleware<TName>; // Factory pattern for parameterized middleware function defineMiddleware<TName extends string, TArgs extends any[]>( name: TName, factory: (...args: TArgs) => MiddlewareHandler ): (...args: TArgs) => NamedMiddleware<TName>;
Factory Middleware
Create parameterized middleware:
typescriptexport const requirePermissions = defineMiddleware('permission', (...permissions: string[]) => async (c, next) => { const user = c.get('user'); const hasAll = permissions.every(p => user.permissions.includes(p)); if (!hasAll) { throw new ForbiddenError({ message: 'Insufficient permissions' }); } await next(); } ); // Usage export const deletePost = route.delete('/posts/:id') .use([requirePermissions('posts:delete', 'admin:write')]) .handler(async (c) => { /* ... */ });
Built-in Middleware
RequestLogger
Automatic API request/response logging with performance monitoring.
typescriptimport { RequestLogger } from '@spfn/core'; // In server config export default defineServerConfig() .middlewares([ RequestLogger() ]) .routes(appRouter) .build(); // With configuration RequestLogger({ excludePaths: ['/health', '/ping'], sensitiveFields: ['password', 'token', 'apiKey'], slowRequestThreshold: 1000, // ms })
Configuration Options
| Option | Type | Default |
|---|---|---|
excludePaths | string[] | ['/health', '/ping'] |
sensitiveFields | string[] | ['password', 'token', ...] |
slowRequestThreshold | number | 1000 (1 second) |
Log Output
json// Request log { "level": "info", "timestamp": "2024-01-15T10:30:00.000Z", "requestId": "req_1705315800000_abc123", "method": "POST", "path": "/users", "body": { "email": "user@example.com", "password": "***" } } // Response log { "level": "info", "requestId": "req_1705315800000_abc123", "status": 201, "duration": 123, "message": "Request completed" }
ErrorHandler
Global error handler for consistent error responses.
typescriptimport { ErrorHandler } from '@spfn/core'; export default defineServerConfig() .middlewares([ ErrorHandler({ includeStack: process.env.NODE_ENV === 'development' }) ]) .routes(appRouter) .build();
Error Response Format
json// Validation error (400) { "error": "ValidationError", "message": "Invalid email format", "statusCode": 400, "details": { "field": "email" } } // Not found error (404) { "error": "NotFoundError", "message": "User not found", "statusCode": 404 } // Internal server error (500) { "error": "InternalServerError", "message": "An unexpected error occurred", "statusCode": 500, "stack": "..." // Only in development }
Transactional
Wraps route handler in a database transaction using AsyncLocalStorage.
typescriptimport { Transactional, getTransaction } from '@spfn/core'; export const createOrder = route.post('/orders') .input({ body: OrderSchema }) .use([Transactional()]) .handler(async (c) => { const { body } = await c.data(); // Get transaction from context const tx = getTransaction(); // All operations use same transaction const order = await create(orders, body, { tx }); await create(orderItems, body.items, { tx }); // Automatic commit on success, rollback on error return c.created(order); });
Configuration Options
typescriptTransactional({ isolationLevel: 'read committed', // Transaction isolation level timeout: 5000, // Transaction timeout (ms) })
Custom Middleware
Basic Middleware
typescriptimport { defineMiddleware } from '@spfn/core/route'; export const timingMiddleware = defineMiddleware('timing', async (c, next) => { const start = Date.now(); await next(); const duration = Date.now() - start; c.header('X-Response-Time', `${duration}ms`); });
Authentication Middleware
typescriptimport { defineMiddleware } from '@spfn/core/route'; import { UnauthorizedError } from '@spfn/core/errors'; export const authMiddleware = defineMiddleware('auth', async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', ''); if (!token) { throw new UnauthorizedError({ message: 'Missing authentication token' }); } const user = await verifyToken(token); if (!user) { throw new UnauthorizedError({ message: 'Invalid token' }); } c.set('user', user); await next(); });
Rate Limiting Middleware
typescriptimport { defineMiddleware } from '@spfn/core/route'; import { TooManyRequestsError } from '@spfn/core/errors'; const rateLimitMap = new Map<string, { count: number; resetAt: number }>(); export const rateLimitMiddleware = defineMiddleware('rateLimit', async (c, next) => { const ip = c.req.header('x-forwarded-for') || 'unknown'; const now = Date.now(); const windowMs = 60000; // 1 minute const max = 100; let record = rateLimitMap.get(ip); if (!record || now > record.resetAt) { record = { count: 0, resetAt: now + windowMs }; rateLimitMap.set(ip, record); } record.count++; if (record.count > max) { throw new TooManyRequestsError({ message: 'Rate limit exceeded' }); } c.header('X-RateLimit-Limit', max.toString()); c.header('X-RateLimit-Remaining', (max - record.count).toString()); await next(); });
Request ID Middleware
typescriptimport { defineMiddleware } from '@spfn/core/route'; import { randomUUID } from 'crypto'; export const requestIdMiddleware = defineMiddleware('requestId', async (c, next) => { const requestId = c.req.header('X-Request-ID') || randomUUID(); c.set('requestId', requestId); c.header('X-Request-ID', requestId); await next(); });
Middleware Application
Global Middleware (Server Config)
typescript// src/server/server.config.ts import { defineServerConfig } from '@spfn/core/server'; import { appRouter } from './router'; import { loggingMiddleware, authMiddleware, rateLimitMiddleware } from './middlewares'; export default defineServerConfig() .middlewares([ loggingMiddleware, authMiddleware, rateLimitMiddleware, ]) .routes(appRouter) .build();
Route-Specific Middleware
typescript// Apply middleware to specific routes using .use() export const deleteUser = route.delete('/admin/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .use([adminOnlyMiddleware, auditLogMiddleware]) .handler(async (c) => { const { params } = await c.data(); await deleteUserById(params.id); return c.noContent(); });
Skipping Global Middleware
typescript// Skip specific global middlewares using .skip() export const healthCheck = route.get('/health') .skip(['auth']) // Skip auth middleware .handler(async (c) => { return { status: 'ok' }; }); // Skip all global middlewares export const internalHealthCheck = route.get('/_internal/health') .skip('*') // Skip all middlewares .handler(async (c) => { return { status: 'ok', timestamp: Date.now() }; });
Middleware Execution Order
Middleware executes in this order:
typescript// 1. Global middleware (from server config) export default defineServerConfig() .middlewares([ loggingMiddleware, // First authMiddleware, // Second rateLimitMiddleware, // Third ]) // 2. Route-specific middleware (.use()) export const route = route.post('/orders') .use([validateOrderMiddleware]) // Fourth .handler(async (c) => { ... }); // Fifth (handler) // Response flows back through middleware in reverse order
Middleware Types
MiddlewareHandler
typescripttype MiddlewareHandler = ( c: Context, next: () => Promise<void> ) => Promise<void | Response>;
NamedMiddleware
typescriptinterface NamedMiddleware<TName extends string = string> { name: TName; handler: MiddlewareHandler; }
Best Practices
1. Keep Middleware Focused
typescript// ✅ Good: Single responsibility export const authMiddleware = defineMiddleware('auth', ...); export const rateLimitMiddleware = defineMiddleware('rateLimit', ...); // ❌ Bad: Multiple responsibilities export const authAndRateLimitMiddleware = defineMiddleware('authAndRateLimit', ...);
2. Always Call next()
typescript// ✅ Good: Call next() export const middleware = defineMiddleware('example', async (c, next) => { // Before handler console.log('Before'); await next(); // Continue to next middleware/handler // After handler console.log('After'); }); // ❌ Bad: Missing next() export const badMiddleware = defineMiddleware('bad', async (c, next) => { console.log('Before'); // Forgot to call next() - request hangs! });
3. Use c.set() for Context Sharing
typescript// ✅ Good: Store in context c.set('user', user); c.set('requestId', requestId); // Handler access via c.raw.get() const user = c.raw.get('user');
4. Handle Errors with Typed Errors
typescriptimport { UnauthorizedError, ForbiddenError } from '@spfn/core/errors'; // ✅ Good: Throw typed errors if (!token) { throw new UnauthorizedError({ message: 'Missing token' }); } // ❌ Bad: Generic error if (!token) { throw new Error('Missing token'); }
5. Use Named Middleware for Skip Support
typescript// ✅ Good: Named middleware can be skipped export const authMiddleware = defineMiddleware('auth', ...); // Routes can skip by name export const publicRoute = route.get('/public') .skip(['auth']) .handler(...);
6. Order Matters
Place logging first, then auth, then rate limiting:
typescriptexport default defineServerConfig() .middlewares([ loggingMiddleware, // First - logs all requests timingMiddleware, // Timing for all requests corsMiddleware, // CORS before auth authMiddleware, // Authentication rateLimitMiddleware, // Rate limiting last ]) .routes(appRouter) .build();
Next: CLI Commands
Learn about Superfunction CLI commands for development and deployment.