erp-suite/apps/shared-libs/core/errors/express-integration.example.ts

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 };