214 lines
5.0 KiB
Markdown
214 lines
5.0 KiB
Markdown
# Middleware Guide - Express Middleware Patterns
|
|
|
|
Complete guide to creating and using middleware in backend microservices.
|
|
|
|
## Table of Contents
|
|
|
|
- [Authentication Middleware](#authentication-middleware)
|
|
- [Audit Middleware with AsyncLocalStorage](#audit-middleware-with-asynclocalstorage)
|
|
- [Error Boundary Middleware](#error-boundary-middleware)
|
|
- [Validation Middleware](#validation-middleware)
|
|
- [Composable Middleware](#composable-middleware)
|
|
- [Middleware Ordering](#middleware-ordering)
|
|
|
|
---
|
|
|
|
## Authentication Middleware
|
|
|
|
### SSOMiddleware Pattern
|
|
|
|
**File:** `/form/src/middleware/SSOMiddleware.ts`
|
|
|
|
```typescript
|
|
export class SSOMiddlewareClient {
|
|
static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void {
|
|
const token = req.cookies.refresh_token;
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, config.tokens.jwt);
|
|
res.locals.claims = decoded;
|
|
res.locals.effectiveUserId = decoded.sub;
|
|
next();
|
|
} catch (error) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Audit Middleware with AsyncLocalStorage
|
|
|
|
### Excellent Pattern from Blog API
|
|
|
|
**File:** `/form/src/middleware/auditMiddleware.ts`
|
|
|
|
```typescript
|
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
|
|
export interface AuditContext {
|
|
userId: string;
|
|
userName?: string;
|
|
impersonatedBy?: string;
|
|
sessionId?: string;
|
|
timestamp: Date;
|
|
requestId: string;
|
|
}
|
|
|
|
export const auditContextStorage = new AsyncLocalStorage<AuditContext>();
|
|
|
|
export function auditMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
const context: AuditContext = {
|
|
userId: res.locals.effectiveUserId || 'anonymous',
|
|
userName: res.locals.claims?.preferred_username,
|
|
impersonatedBy: res.locals.isImpersonating ? res.locals.originalUserId : undefined,
|
|
timestamp: new Date(),
|
|
requestId: req.id || uuidv4(),
|
|
};
|
|
|
|
auditContextStorage.run(context, () => {
|
|
next();
|
|
});
|
|
}
|
|
|
|
// Getter for current context
|
|
export function getAuditContext(): AuditContext | null {
|
|
return auditContextStorage.getStore() || null;
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Context propagates through entire request
|
|
- No need to pass context through every function
|
|
- Automatically available in services, repositories
|
|
- Type-safe context access
|
|
|
|
**Usage in Services:**
|
|
```typescript
|
|
import { getAuditContext } from '../middleware/auditMiddleware';
|
|
|
|
async function someOperation() {
|
|
const context = getAuditContext();
|
|
console.log('Operation by:', context?.userId);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Boundary Middleware
|
|
|
|
### Comprehensive Error Handler
|
|
|
|
**File:** `/form/src/middleware/errorBoundary.ts`
|
|
|
|
```typescript
|
|
export function errorBoundary(
|
|
error: Error,
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void {
|
|
// Determine status code
|
|
const statusCode = getStatusCodeForError(error);
|
|
|
|
// Capture to Sentry
|
|
Sentry.withScope((scope) => {
|
|
scope.setLevel(statusCode >= 500 ? 'error' : 'warning');
|
|
scope.setTag('error_type', error.name);
|
|
scope.setContext('error_details', {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
});
|
|
Sentry.captureException(error);
|
|
});
|
|
|
|
// User-friendly response
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
error: {
|
|
message: getUserFriendlyMessage(error),
|
|
code: error.name,
|
|
},
|
|
requestId: Sentry.getCurrentScope().getPropagationContext().traceId,
|
|
});
|
|
}
|
|
|
|
// Async wrapper
|
|
export function asyncErrorWrapper(
|
|
handler: (req: Request, res: Response, next: NextFunction) => Promise<any>
|
|
) {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
await handler(req, res, next);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Composable Middleware
|
|
|
|
### withAuthAndAudit Pattern
|
|
|
|
```typescript
|
|
export function withAuthAndAudit(...authMiddleware: any[]) {
|
|
return [
|
|
...authMiddleware,
|
|
auditMiddleware,
|
|
];
|
|
}
|
|
|
|
// Usage
|
|
router.post('/:formID/submit',
|
|
...withAuthAndAudit(SSOMiddlewareClient.verifyLoginStatus),
|
|
async (req, res) => controller.submit(req, res)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Middleware Ordering
|
|
|
|
### Critical Order (Must Follow)
|
|
|
|
```typescript
|
|
// 1. Sentry request handler (FIRST)
|
|
app.use(Sentry.Handlers.requestHandler());
|
|
|
|
// 2. Body parsing
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// 3. Cookie parsing
|
|
app.use(cookieParser());
|
|
|
|
// 4. Auth initialization
|
|
app.use(SSOMiddleware.initialize());
|
|
|
|
// 5. Routes registered here
|
|
app.use('/api/users', userRoutes);
|
|
|
|
// 6. Error handler (AFTER routes)
|
|
app.use(errorBoundary);
|
|
|
|
// 7. Sentry error handler (LAST)
|
|
app.use(Sentry.Handlers.errorHandler());
|
|
```
|
|
|
|
**Rule:** Error handlers MUST be registered AFTER all routes!
|
|
|
|
---
|
|
|
|
**Related Files:**
|
|
- SKILL.md
|
|
- [routing-and-controllers.md](routing-and-controllers.md)
|
|
- [async-and-errors.md](async-and-errors.md)
|