8.3 KiB
trigger
| trigger |
|---|
| always_on |
Middleware Patterns
When to Use This Skill
Use this skill when:
- Creating custom Express middleware
- Organizing middleware chains and ordering
- Implementing authentication/authorization middleware
- Creating request/response transformation middleware
- Handling cross-cutting concerns (logging, metrics, validation)
- Implementing async middleware patterns
- Testing middleware implementations
Core Concepts
Middleware Function Signature
Express middleware functions have this signature:
(req: Request, res: Response, next: NextFunction) => void | Promise<void>
Middleware Types
- Application-level: Applied to all routes (
app.use()) - Router-level: Applied to specific routes (
router.use()) - Route-level: Applied to specific route handlers
Middleware Execution Order
Critical: Middleware order matters! Execution flows top-to-bottom:
Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response
Patterns
Middleware Chain Order
Standard middleware order in GoodGo services:
// 1. Security (Helmet, CORS)
app.use(helmet());
app.use(cors({ ... }));
// 2. Rate Limiting
app.use('/api', rateLimitMiddleware);
// 3. Correlation ID (early for tracing)
app.use(correlationMiddleware());
// 4. Body Parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// 5. Request Logging
app.use(loggerMiddleware);
// 6. Metrics
app.use(metricsMiddleware);
// 7. Routes
app.use(createRouter());
// 8. Error Handling (ALWAYS LAST)
app.use(notFoundHandler);
app.use(errorHandler);
Correlation Middleware Pattern
Adds correlation ID for request tracing:
export const correlationMiddleware = () => {
return (req: Request, res: Response, next: NextFunction) => {
const correlationId = req.headers['x-correlation-id'] || generateId();
req.correlationId = correlationId;
res.setHeader('x-correlation-id', correlationId);
next();
};
};
Authentication Middleware Pattern
Verifies JWT tokens and attaches user to request:
export const authenticate = () => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const token = extractToken(req);
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
const payload = await jwtService.verify(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
};
Validation Middleware Pattern
Validates request data using Zod:
export const validateDto = (schema: AnyZodObject, property: 'body' | 'query' | 'params' = 'body') => {
return (req: Request, res: Response, next: NextFunction) => {
try {
const validatedData = schema.parse(req[property]);
(req as any)[property] = validatedData;
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
details: error.errors,
},
});
}
next(error);
}
};
};
Conditional Middleware
Apply middleware conditionally:
const conditionalAuth = (options: { optional?: boolean } = {}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const token = extractToken(req);
if (token) {
const payload = await jwtService.verify(token);
req.user = payload;
} else if (!options.optional) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
} catch (error) {
if (!options.optional) {
return res.status(401).json({ error: 'Invalid token' });
}
next();
}
};
};
Async Middleware Pattern
Handle async operations properly:
export const asyncMiddleware = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage
app.get('/users', asyncMiddleware(async (req, res) => {
const users = await userService.findAll();
res.json({ success: true, data: users });
}));
Request/Response Transformation
Transform request or response data:
export const transformResponse = () => {
return (req: Request, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = function(data: unknown) {
const transformed = {
success: true,
data,
timestamp: new Date().toISOString(),
};
return originalJson(transformed);
};
next();
};
};
Logging Middleware Pattern
Log request details:
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration,
correlationId: req.correlationId,
});
});
next();
};
Best Practices
- Order Matters: Place middleware in correct order (security → correlation → parsing → logging → routes → errors)
- Error Handling: Always handle errors and call
next(error)for error middleware - Async Support: Wrap async middleware properly to catch promise rejections
- Early Returns: Use early returns for validation failures (don't call
next()) - Request Extension: Use TypeScript declaration merging to extend Request type
- Conditional Logic: Use middleware factories for conditional middleware
- Reusability: Create reusable middleware functions
- Performance: Keep middleware lightweight, avoid heavy operations
Common Mistakes
- Wrong Order: Placing middleware in incorrect order (e.g., error handler before routes)
- Not Calling Next: Forgetting to call
next()ornext(error) - Async Errors: Not handling promise rejections in async middleware
- Early Return Issues: Calling
next()after sending response - Type Safety: Not extending Express Request type properly
- Performance: Doing heavy operations in middleware
Troubleshooting
Middleware Not Executing
Problem: Middleware not being called
Solution: Check middleware order, ensure it's added before routes. Verify next() is called.
Async Errors Not Caught
Problem: Unhandled promise rejections in async middleware
Solution: Use asyncHandler wrapper or wrap async code in try-catch with next(error).
Quick Reference
Middleware Order (Critical):
1. Security (helmet, cors)
2. Rate Limiting
3. Correlation ID
4. Body Parsing (json, urlencoded)
5. Request Logging
6. Metrics
7. Routes
8. Error Handling (LAST)
| Middleware Type | Signature | When to Use |
|---|---|---|
| Standard | (req, res, next) => void |
Sync operations |
| Async | async (req, res, next) => Promise<void> |
Async operations |
| Error | (err, req, res, next) => void |
Error handling |
Common Patterns:
// Async wrapper
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Conditional middleware
const conditionalMiddleware = (condition, middleware) =>
condition ? middleware : (req, res, next) => next();
// Factory pattern
const authenticate = (options = {}) => async (req, res, next) => { /* ... */ };
Essential Imports:
import { Request, Response, NextFunction } from 'express';
import { z, ZodError } from 'zod';
import { logger } from '@goodgo/logger';
Resources
- Express Middleware - Express middleware guide
- Error Handling - Error middleware patterns
- Security - Auth middleware patterns
- API Design - Request/response patterns
- Resilience Patterns - Circuit breaker middleware
- Observability & Monitoring - Logging middleware
- Project Rules - GoodGo standards