- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
12 KiB
name, description
| name | description |
|---|---|
| middleware-patterns | Express middleware patterns and best practices for GoodGo microservices. Use when creating custom middleware, organizing middleware chains, handling request/response transformation, or implementing cross-cutting concerns. |
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
The following diagram illustrates the complete middleware chain flow in GoodGo services:
flowchart TD
Start([HTTP Request]) --> Security["Security Middleware<br/>Helmet, CORS"]
Security --> RateLimit["Rate Limiting<br/>Middleware"]
RateLimit --> Correlation["Correlation ID<br/>Middleware"]
Correlation --> BodyParsing["Body Parsing<br/>JSON, URLEncoded, Cookies"]
BodyParsing --> Logging["Request Logging<br/>Middleware"]
Logging --> Metrics["Metrics Collection<br/>Middleware"]
Metrics --> Routes["Route Handlers"]
Routes -->|Success| Response([HTTP Response])
Routes -->|Error| ErrorHandler["Error Handler<br/>Middleware"]
Routes -->|Not Found| NotFound["Not Found<br/>Handler"]
ErrorHandler --> Response
NotFound --> 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' });
}
};
};
The following sequence diagram illustrates the authentication middleware flow:
sequenceDiagram
participant Client
participant AuthMW as Authentication Middleware
participant JWTService as JWT Service
participant RouteHandler as Route Handler
Client->>AuthMW: HTTP Request with Token
AuthMW->>AuthMW: Extract token from headers
alt Token exists
AuthMW->>JWTService: Verify token
alt Token valid
JWTService-->>AuthMW: Payload (user data)
AuthMW->>AuthMW: Attach user to req.user
AuthMW->>RouteHandler: next() - Continue
RouteHandler->>Client: HTTP Response (200)
else Token invalid
JWTService-->>AuthMW: Verification error
AuthMW->>Client: HTTP Response (401 Unauthorized)
end
else No token
AuthMW->>Client: HTTP Response (401 Unauthorized)
end
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);
}
};
};
The following sequence diagram shows the validation middleware flow:
sequenceDiagram
participant Client
participant ValidateMW as Validation Middleware
participant ZodSchema as Zod Schema
participant RouteHandler as Route Handler
Client->>ValidateMW: HTTP Request with Data
ValidateMW->>ValidateMW: Extract data from req[property]
ValidateMW->>ZodSchema: schema.parse(data)
alt Validation successful
ZodSchema-->>ValidateMW: Validated data
ValidateMW->>ValidateMW: Replace req[property] with validated data
ValidateMW->>RouteHandler: next() - Continue
RouteHandler->>Client: HTTP Response (200)
else Validation failed
ZodSchema-->>ValidateMW: ZodError with details
ValidateMW->>ValidateMW: Format error response
ValidateMW->>Client: HTTP Response (400 Validation Error)
end
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 });
}));
The following sequence diagram illustrates async middleware error handling:
sequenceDiagram
participant Client
participant AsyncMW as Async Middleware Wrapper
participant AsyncHandler as Async Route Handler
participant ErrorHandler as Error Handler
Client->>AsyncMW: HTTP Request
AsyncMW->>AsyncHandler: Execute async function
alt Async operation succeeds
AsyncHandler->>AsyncHandler: Process request
AsyncHandler->>Client: HTTP Response (200)
else Async operation fails
AsyncHandler-->>AsyncMW: Promise rejection (Error)
AsyncMW->>ErrorHandler: next(error)
ErrorHandler->>ErrorHandler: Format error response
ErrorHandler->>Client: HTTP Response (500 Error)
end
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: any) {
const transformed = {
success: true,
data,
timestamp: new Date().toISOString(),
};
return originalJson(transformed);
};
next();
};
};
The following sequence diagram shows how request and response transformation middleware works:
sequenceDiagram
participant Client
participant TransformMW as Transform Middleware
participant RouteHandler as Route Handler
participant Response as Response Object
Client->>TransformMW: HTTP Request
Note over TransformMW: Intercept res.json()
TransformMW->>TransformMW: Store original res.json
TransformMW->>TransformMW: Override res.json with transformation logic
TransformMW->>RouteHandler: next() - Continue chain
RouteHandler->>RouteHandler: Process request, Generate data
RouteHandler->>Response: res.json(rawData)
Note over Response: Transformed res.json executes
Response->>Response: Wrap data: success, data, timestamp
Response->>Client: HTTP Response (transformed)
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();
};
The following sequence diagram illustrates how logging middleware tracks request lifecycle:
sequenceDiagram
participant Client
participant LogMW as Logging Middleware
participant RouteHandler as Route Handler
participant Logger as Logger Service
participant Response as Response Object
Client->>LogMW: HTTP Request
LogMW->>LogMW: Record startTime = Date.now()
LogMW->>RouteHandler: next() - Continue chain
RouteHandler->>RouteHandler: Process request
RouteHandler->>Response: res.json(data) or res.send()
Response->>Response: Set statusCode, send response
Response->>Response: Emit 'finish' event
Response->>LogMW: 'finish' event triggered
LogMW->>LogMW: Calculate duration = Date.now() - startTime
LogMW->>Logger: logger.info('Request completed', {<br/>method, url, statusCode,<br/>duration, correlationId})
Logger->>Logger: Write structured log entry
Response->>Client: HTTP Response
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).
Resources
- Correlation Middleware
- Auth Middleware
- Validation Middleware
- Error Handling - Error middleware patterns