465 lines
11 KiB
TypeScript
465 lines
11 KiB
TypeScript
/**
|
|
* Express Integration Example
|
|
*
|
|
* This file demonstrates how to integrate the error handling system
|
|
* into an Express application.
|
|
*
|
|
* @example Integration Steps:
|
|
* 1. Set up error middleware (must be last)
|
|
* 2. Optionally add 404 handler
|
|
* 3. Use custom error classes in your routes
|
|
* 4. Configure request ID generation (optional)
|
|
*/
|
|
|
|
import express, { Request, Response, NextFunction, Router } from 'express';
|
|
import {
|
|
createErrorMiddleware,
|
|
notFoundMiddleware,
|
|
NotFoundError,
|
|
ValidationError,
|
|
UnauthorizedError,
|
|
BadRequestError,
|
|
BaseError,
|
|
} from '@erp-suite/core';
|
|
|
|
// ========================================
|
|
// Basic Express App Setup
|
|
// ========================================
|
|
|
|
/**
|
|
* Create Express app with error handling
|
|
*/
|
|
function createApp() {
|
|
const app = express();
|
|
|
|
// Body parsing middleware
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// Request ID middleware (optional but recommended)
|
|
app.use(requestIdMiddleware);
|
|
|
|
// Your routes
|
|
app.use('/api/users', usersRouter);
|
|
app.use('/api/products', productsRouter);
|
|
|
|
// 404 handler (must be after all routes)
|
|
app.use(notFoundMiddleware);
|
|
|
|
// Error handling middleware (must be last)
|
|
app.use(createErrorMiddleware({
|
|
includeStackTrace: process.env.NODE_ENV !== 'production',
|
|
}));
|
|
|
|
return app;
|
|
}
|
|
|
|
// ========================================
|
|
// Request ID Middleware (Optional)
|
|
// ========================================
|
|
|
|
import { randomUUID } from 'crypto';
|
|
|
|
/**
|
|
* Middleware to generate and track request IDs
|
|
*/
|
|
function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
const requestId =
|
|
(req.headers['x-request-id'] as string) ||
|
|
(req.headers['x-correlation-id'] as string) ||
|
|
randomUUID();
|
|
|
|
// Attach to request for access in handlers
|
|
(req as any).requestId = requestId;
|
|
|
|
// Include in response headers
|
|
res.setHeader('X-Request-ID', requestId);
|
|
|
|
next();
|
|
}
|
|
|
|
// ========================================
|
|
// Custom Logger Integration
|
|
// ========================================
|
|
|
|
import { ErrorLogger } from '@erp-suite/core';
|
|
|
|
/**
|
|
* Custom logger implementation (e.g., Winston, Pino)
|
|
*/
|
|
class WinstonLogger implements ErrorLogger {
|
|
error(message: string, ...meta: any[]): void {
|
|
// winston.error(message, ...meta);
|
|
console.error('[ERROR]', message, ...meta);
|
|
}
|
|
|
|
warn(message: string, ...meta: any[]): void {
|
|
// winston.warn(message, ...meta);
|
|
console.warn('[WARN]', message, ...meta);
|
|
}
|
|
|
|
log(message: string, ...meta: any[]): void {
|
|
// winston.info(message, ...meta);
|
|
console.log('[INFO]', message, ...meta);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* App with custom logger
|
|
*/
|
|
function createAppWithCustomLogger() {
|
|
const app = express();
|
|
|
|
app.use(express.json());
|
|
app.use('/api/users', usersRouter);
|
|
|
|
// Error middleware with custom logger
|
|
app.use(createErrorMiddleware({
|
|
logger: new WinstonLogger(),
|
|
includeStackTrace: process.env.NODE_ENV !== 'production',
|
|
}));
|
|
|
|
return app;
|
|
}
|
|
|
|
// ========================================
|
|
// Users Router Example
|
|
// ========================================
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
}
|
|
|
|
// Mock database
|
|
const users: User[] = [
|
|
{ id: '1', email: 'user1@example.com', name: 'User One' },
|
|
{ id: '2', email: 'user2@example.com', name: 'User Two' },
|
|
];
|
|
|
|
const usersRouter = Router();
|
|
|
|
/**
|
|
* GET /api/users/:id
|
|
*
|
|
* Demonstrates NotFoundError handling
|
|
*/
|
|
usersRouter.get('/:id', (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const user = users.find(u => u.id === req.params.id);
|
|
|
|
if (!user) {
|
|
throw new NotFoundError('User not found', { userId: req.params.id });
|
|
}
|
|
|
|
res.json(user);
|
|
} catch (error) {
|
|
next(error); // Pass to error middleware
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/users
|
|
*
|
|
* Demonstrates ValidationError handling
|
|
*/
|
|
usersRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { email, name } = req.body;
|
|
|
|
// Validation
|
|
const errors: any[] = [];
|
|
|
|
if (!email || !email.includes('@')) {
|
|
errors.push({ field: 'email', message: 'Valid email is required' });
|
|
}
|
|
|
|
if (!name || name.length < 2) {
|
|
errors.push({ field: 'name', message: 'Name must be at least 2 characters' });
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw new ValidationError('Validation failed', { errors });
|
|
}
|
|
|
|
// Check for duplicate email
|
|
const existing = users.find(u => u.email === email);
|
|
if (existing) {
|
|
throw new BadRequestError('Email already exists', {
|
|
email,
|
|
existingUserId: existing.id,
|
|
});
|
|
}
|
|
|
|
// Create user
|
|
const user: User = {
|
|
id: String(users.length + 1),
|
|
email,
|
|
name,
|
|
};
|
|
|
|
users.push(user);
|
|
|
|
res.status(201).json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Async/await route handler with error handling
|
|
*/
|
|
usersRouter.get(
|
|
'/:id/profile',
|
|
asyncHandler(async (req: Request, res: Response) => {
|
|
const user = await findUserById(req.params.id);
|
|
const profile = await getUserProfile(user.id);
|
|
|
|
res.json(profile);
|
|
})
|
|
);
|
|
|
|
// ========================================
|
|
// Async Handler Wrapper
|
|
// ========================================
|
|
|
|
/**
|
|
* Wraps async route handlers to automatically catch errors
|
|
*
|
|
* Usage: app.get('/route', asyncHandler(async (req, res) => { ... }))
|
|
*/
|
|
function asyncHandler(
|
|
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
|
|
) {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
};
|
|
}
|
|
|
|
// ========================================
|
|
// Products Router Example
|
|
// ========================================
|
|
|
|
const productsRouter = Router();
|
|
|
|
/**
|
|
* Protected route example
|
|
*/
|
|
productsRouter.get(
|
|
'/',
|
|
authMiddleware, // Authentication middleware
|
|
asyncHandler(async (req: Request, res: Response) => {
|
|
// This route is protected and will throw UnauthorizedError
|
|
// if authentication fails
|
|
const products = await getProducts();
|
|
res.json(products);
|
|
})
|
|
);
|
|
|
|
/**
|
|
* Authentication middleware example
|
|
*/
|
|
function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
|
|
if (!token) {
|
|
throw new UnauthorizedError('Authentication token required');
|
|
}
|
|
|
|
// Validate token...
|
|
if (token !== 'valid-token') {
|
|
throw new UnauthorizedError('Invalid or expired token', {
|
|
providedToken: token.substring(0, 10) + '...',
|
|
});
|
|
}
|
|
|
|
// Attach user to request
|
|
(req as any).user = { id: '1', email: 'user@example.com' };
|
|
next();
|
|
}
|
|
|
|
// ========================================
|
|
// Service Layer Example
|
|
// ========================================
|
|
|
|
/**
|
|
* Service layer with error handling
|
|
*/
|
|
class UserService {
|
|
async findById(id: string): Promise<User> {
|
|
const user = users.find(u => u.id === id);
|
|
|
|
if (!user) {
|
|
throw new NotFoundError('User not found', { userId: id });
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
async create(email: string, name: string): Promise<User> {
|
|
// Validation
|
|
if (!email || !email.includes('@')) {
|
|
throw new ValidationError('Invalid email address', {
|
|
field: 'email',
|
|
value: email,
|
|
});
|
|
}
|
|
|
|
// Business logic validation
|
|
const existing = users.find(u => u.email === email);
|
|
if (existing) {
|
|
throw new BadRequestError('Email already exists', {
|
|
email,
|
|
existingUserId: existing.id,
|
|
});
|
|
}
|
|
|
|
const user: User = {
|
|
id: String(users.length + 1),
|
|
email,
|
|
name,
|
|
};
|
|
|
|
users.push(user);
|
|
return user;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Custom Domain Errors
|
|
// ========================================
|
|
|
|
/**
|
|
* Create custom errors for your domain
|
|
*/
|
|
class InsufficientBalanceError extends BaseError {
|
|
readonly statusCode = 400;
|
|
readonly error = 'Insufficient Balance';
|
|
|
|
constructor(required: number, available: number) {
|
|
super('Insufficient balance for this operation', {
|
|
required,
|
|
available,
|
|
deficit: required - available,
|
|
});
|
|
}
|
|
}
|
|
|
|
class PaymentService {
|
|
async processPayment(userId: string, amount: number): Promise<void> {
|
|
const balance = await this.getBalance(userId);
|
|
|
|
if (balance < amount) {
|
|
throw new InsufficientBalanceError(amount, balance);
|
|
}
|
|
|
|
// Process payment...
|
|
}
|
|
|
|
private async getBalance(userId: string): Promise<number> {
|
|
return 100; // Mock
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Error Response Examples
|
|
// ========================================
|
|
|
|
/**
|
|
* Example error responses generated by the system:
|
|
*
|
|
* 404 Not Found:
|
|
* GET /api/users/999
|
|
* {
|
|
* "statusCode": 404,
|
|
* "error": "Not Found",
|
|
* "message": "User not found",
|
|
* "details": { "userId": "999" },
|
|
* "timestamp": "2025-12-12T10:30:00.000Z",
|
|
* "path": "/api/users/999",
|
|
* "requestId": "550e8400-e29b-41d4-a716-446655440000"
|
|
* }
|
|
*
|
|
* 422 Validation Error:
|
|
* POST /api/users { "email": "invalid", "name": "A" }
|
|
* {
|
|
* "statusCode": 422,
|
|
* "error": "Validation Error",
|
|
* "message": "Validation failed",
|
|
* "details": {
|
|
* "errors": [
|
|
* { "field": "email", "message": "Valid email is required" },
|
|
* { "field": "name", "message": "Name must be at least 2 characters" }
|
|
* ]
|
|
* },
|
|
* "timestamp": "2025-12-12T10:30:00.000Z",
|
|
* "path": "/api/users",
|
|
* "requestId": "550e8400-e29b-41d4-a716-446655440001"
|
|
* }
|
|
*
|
|
* 401 Unauthorized:
|
|
* GET /api/products (without token)
|
|
* {
|
|
* "statusCode": 401,
|
|
* "error": "Unauthorized",
|
|
* "message": "Authentication token required",
|
|
* "timestamp": "2025-12-12T10:30:00.000Z",
|
|
* "path": "/api/products",
|
|
* "requestId": "550e8400-e29b-41d4-a716-446655440002"
|
|
* }
|
|
*
|
|
* 400 Bad Request:
|
|
* POST /api/users { "email": "existing@example.com", "name": "Test" }
|
|
* {
|
|
* "statusCode": 400,
|
|
* "error": "Bad Request",
|
|
* "message": "Email already exists",
|
|
* "details": {
|
|
* "email": "existing@example.com",
|
|
* "existingUserId": "1"
|
|
* },
|
|
* "timestamp": "2025-12-12T10:30:00.000Z",
|
|
* "path": "/api/users",
|
|
* "requestId": "550e8400-e29b-41d4-a716-446655440003"
|
|
* }
|
|
*/
|
|
|
|
// ========================================
|
|
// Helper Functions (Mock)
|
|
// ========================================
|
|
|
|
async function findUserById(id: string): Promise<User> {
|
|
const user = users.find(u => u.id === id);
|
|
if (!user) {
|
|
throw new NotFoundError('User not found', { userId: id });
|
|
}
|
|
return user;
|
|
}
|
|
|
|
async function getUserProfile(userId: string): Promise<any> {
|
|
return { userId, bio: 'User bio', avatar: 'avatar.jpg' };
|
|
}
|
|
|
|
async function getProducts(): Promise<any[]> {
|
|
return [
|
|
{ id: '1', name: 'Product 1', price: 100 },
|
|
{ id: '2', name: 'Product 2', price: 200 },
|
|
];
|
|
}
|
|
|
|
// ========================================
|
|
// Start Server
|
|
// ========================================
|
|
|
|
if (require.main === module) {
|
|
const app = createApp();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
}
|
|
|
|
export { createApp, createAppWithCustomLogger, asyncHandler };
|