| .. | ||
| base-error.ts | ||
| error-filter.ts | ||
| error-middleware.ts | ||
| express-integration.example.ts | ||
| http-errors.ts | ||
| IMPLEMENTATION_SUMMARY.md | ||
| index.ts | ||
| INTEGRATION_GUIDE.md | ||
| nestjs-integration.example.ts | ||
| QUICK_REFERENCE.md | ||
| README.md | ||
| STRUCTURE.md | ||
Error Handling System
Standardized error handling for all ERP-Suite backends (NestJS and Express).
Overview
This module provides:
- Base error classes with consistent structure
- HTTP-specific errors for common status codes
- NestJS exception filter for automatic error handling
- Express middleware for error handling
- Request tracking with request IDs
- Structured logging with severity levels
Installation
The error handling module is part of @erp-suite/core:
import {
// Base types
BaseError,
ErrorResponse,
// HTTP errors
BadRequestError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
ValidationError,
InternalServerError,
// NestJS
GlobalExceptionFilter,
// Express
createErrorMiddleware,
errorMiddleware,
notFoundMiddleware,
} from '@erp-suite/core';
Quick Start
NestJS Integration
// main.ts
import { NestFactory } from '@nestjs/core';
import { GlobalExceptionFilter } from '@erp-suite/core';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Register global exception filter
app.useGlobalFilters(new GlobalExceptionFilter());
await app.listen(3000);
}
Express Integration
// index.ts
import express from 'express';
import { createErrorMiddleware, notFoundMiddleware } from '@erp-suite/core';
const app = express();
// Your routes here
app.use('/api', routes);
// 404 handler (before error middleware)
app.use(notFoundMiddleware);
// Error handler (must be last)
app.use(createErrorMiddleware());
app.listen(3000);
Error Classes
HTTP Errors
All HTTP error classes extend BaseError and include:
statusCode: HTTP status codeerror: Error type stringmessage: Human-readable messagedetails: Optional additional context
Available Error Classes
| Class | Status Code | Usage |
|---|---|---|
BadRequestError |
400 | Invalid request parameters |
UnauthorizedError |
401 | Missing or invalid authentication |
ForbiddenError |
403 | Authenticated but insufficient permissions |
NotFoundError |
404 | Resource doesn't exist |
ConflictError |
409 | Resource conflict (e.g., duplicate) |
ValidationError |
422 | Validation failed |
InternalServerError |
500 | Unexpected server error |
Usage Examples
// Not Found
throw new NotFoundError('User not found', { userId: '123' });
// Validation
throw new ValidationError('Invalid input', {
errors: [
{ field: 'email', message: 'Invalid format' },
{ field: 'age', message: 'Must be 18+' }
]
});
// Unauthorized
throw new UnauthorizedError('Invalid token');
// Conflict
throw new ConflictError('Email already exists', { email: 'user@example.com' });
Custom Domain Errors
Create custom errors for your domain:
import { BaseError } from '@erp-suite/core';
export 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,
});
}
}
// Usage
throw new InsufficientBalanceError(100, 50);
Error Response Format
All errors are converted to this standardized format:
interface ErrorResponse {
statusCode: number; // HTTP status code
error: string; // Error type
message: string; // Human-readable message
details?: object; // Optional additional context
timestamp: string; // ISO 8601 timestamp
path?: string; // Request path
requestId?: string; // Request tracking ID
}
Example Response
{
"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"
}
NestJS Details
Global Filter Registration
Option 1: In main.ts (Recommended for simple cases)
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new GlobalExceptionFilter());
Option 2: As Provider (Recommended for DI support)
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
],
})
export class AppModule {}
Using in Controllers/Services
@Injectable()
export class UsersService {
async findById(id: string): Promise<User> {
const user = await this.repository.findOne({ where: { id } });
if (!user) {
throw new NotFoundError('User not found', { userId: id });
}
return user;
}
}
@Controller('users')
export class UsersController {
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> {
// Errors are automatically caught and formatted
return this.usersService.findById(id);
}
}
Request ID Tracking
import { Injectable, NestMiddleware } from '@nestjs/common';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] || randomUUID();
req.headers['x-request-id'] = requestId;
res.setHeader('X-Request-ID', requestId);
next();
}
}
Express Details
Middleware Setup
import express from 'express';
import {
createErrorMiddleware,
notFoundMiddleware,
ErrorLogger,
} from '@erp-suite/core';
const app = express();
// Body parsing
app.use(express.json());
// Your routes
app.use('/api/users', usersRouter);
// 404 handler (optional but recommended)
app.use(notFoundMiddleware);
// Error middleware (MUST be last)
app.use(createErrorMiddleware({
logger: customLogger,
includeStackTrace: process.env.NODE_ENV !== 'production',
}));
Configuration Options
interface ErrorMiddlewareOptions {
logger?: ErrorLogger; // Custom logger
includeStackTrace?: boolean; // Include stack traces (dev only)
transformer?: (error, response) => response; // Custom transformer
}
Custom Logger
import { ErrorLogger } from '@erp-suite/core';
class CustomLogger implements ErrorLogger {
error(message: string, ...meta: any[]): void {
winston.error(message, ...meta);
}
warn(message: string, ...meta: any[]): void {
winston.warn(message, ...meta);
}
log(message: string, ...meta: any[]): void {
winston.info(message, ...meta);
}
}
app.use(createErrorMiddleware({
logger: new CustomLogger(),
}));
Async Route Handlers
Use an async handler wrapper to automatically catch errors:
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await findUserById(req.params.id);
res.json(user);
}));
Using in Routes
const router = express.Router();
router.get('/:id', async (req, res, next) => {
try {
const user = await usersService.findById(req.params.id);
res.json(user);
} catch (error) {
next(error); // Pass to error middleware
}
});
Logging
Log Levels
Errors are automatically logged with appropriate severity:
- ERROR (500+): Server errors, unexpected errors
- WARN (400-499): Client errors, validation failures
- INFO (<400): Informational messages
Log Format
// Error log
[500] Internal Server Error: Database connection failed
{
"path": "/api/users",
"requestId": "req-123",
"details": { ... },
"stack": "Error: ...\n at ..."
}
// Warning log
[404] Not Found: User not found
{
"path": "/api/users/999",
"requestId": "req-124",
"details": { "userId": "999" }
}
Best Practices
1. Use Specific Error Classes
// Good
throw new NotFoundError('User not found', { userId });
// Avoid
throw new Error('Not found');
2. Include Contextual Details
// Good - includes helpful context
throw new ValidationError('Validation failed', {
errors: [
{ field: 'email', message: 'Invalid format' },
{ field: 'password', message: 'Too short' }
]
});
// Less helpful
throw new ValidationError('Invalid input');
3. Throw Early, Handle Centrally
// Service layer - throw errors
async findById(id: string): Promise<User> {
const user = await this.repository.findOne(id);
if (!user) {
throw new NotFoundError('User not found', { userId: id });
}
return user;
}
// Controller/Route - let filter/middleware handle
@Get(':id')
async getUser(@Param('id') id: string) {
return this.service.findById(id); // Don't try/catch here
}
4. Don't Expose Internal Details in Production
// Good
throw new InternalServerError('Database operation failed');
// Avoid in production
throw new InternalServerError('Connection to PostgreSQL at 10.0.0.5:5432 failed');
5. Use Request IDs for Tracking
Always include request ID middleware to enable request tracing across logs.
Migration Guide
From Manual Error Handling (NestJS)
Before:
@Get(':id')
async getUser(@Param('id') id: string) {
try {
const user = await this.service.findById(id);
return user;
} catch (error) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
}
After:
@Get(':id')
async getUser(@Param('id') id: string) {
return this.service.findById(id); // Service throws NotFoundError
}
// In service
async findById(id: string) {
const user = await this.repository.findOne(id);
if (!user) {
throw new NotFoundError('User not found', { userId: id });
}
return user;
}
From Manual Error Handling (Express)
Before:
app.get('/users/:id', async (req, res) => {
try {
const user = await findUser(req.params.id);
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Internal error' });
}
});
After:
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await findUser(req.params.id); // Throws NotFoundError
res.json(user);
}));
// Error middleware handles it automatically
Testing
Testing Error Responses (NestJS)
it('should return 404 when user not found', async () => {
const response = await request(app.getHttpServer())
.get('/users/999')
.expect(404);
expect(response.body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'User not found',
details: { userId: '999' },
});
expect(response.body.timestamp).toBeDefined();
expect(response.body.path).toBe('/users/999');
});
Testing Error Responses (Express)
it('should return 404 when user not found', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body).toMatchObject({
statusCode: 404,
error: 'Not Found',
message: 'User not found',
});
});
Testing Custom Errors
describe('InsufficientBalanceError', () => {
it('should create error with correct details', () => {
const error = new InsufficientBalanceError(100, 50);
expect(error.statusCode).toBe(400);
expect(error.message).toBe('Insufficient balance for this operation');
expect(error.details).toEqual({
required: 100,
available: 50,
deficit: 50,
});
});
});
Examples
See detailed integration examples:
- NestJS:
nestjs-integration.example.ts - Express:
express-integration.example.ts
Files
errors/
├── base-error.ts # Base error class and types
├── http-errors.ts # HTTP-specific error classes
├── error-filter.ts # NestJS exception filter
├── error-middleware.ts # Express error middleware
├── index.ts # Module exports
├── README.md # This file
├── nestjs-integration.example.ts # NestJS examples
└── express-integration.example.ts # Express examples
Support
For questions or issues, contact the ERP-Suite development team.