React & Next.js Topic
NestJs Validation & Error Handling
1. Validation Overview
Validation ensures that incoming data meets specific criteria before processing. Error handling manages exceptions gracefully and returns appropriate responses.
Why Validation Matters:
-
Data Integrity: Prevent invalid data from entering your system
-
Security: Protect against injection attacks
-
User Experience: Provide clear error messages
-
Type Safety: Ensure data matches expected types
2. Setting Up Class Validator & Transformer
Installation:
bash
npm install class-validator class-transformer npm install --save-dev @types/class-validator
Basic Setup in main.ts:
typescript
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip properties not in DTO
forbidNonWhitelisted: true, // Throw error if extra properties
transform: true, // Transform payload to DTO instance
transformOptions: {
enableImplicitConversion: true, // Convert types automatically
},
}));
await app.listen(3000);
}
bootstrap();
3. DTO Validation with Decorators
Basic DTO with Validation:
typescript
// dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsNotEmpty,
MinLength,
MaxLength,
IsOptional,
IsInt,
Min,
Max,
IsBoolean,
IsDate,
IsEnum,
IsArray,
ArrayMinSize,
ArrayMaxSize,
ValidateNested,
Matches,
IsUrl,
IsUUID,
IsNumber,
IsPositive,
IsPhoneNumber,
IsDateString,
Length,
} from 'class-validator';
import { Type } from 'class-transformer';
// Custom validation class for nested objects
class AddressDto {
@IsString()
@IsNotEmpty()
street: string;
@IsString()
@IsNotEmpty()
city: string;
@IsString()
@Length(5, 10)
zipCode: string;
@IsString()
@IsNotEmpty()
country: string;
}
// Enum for validation
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
}
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
@Matches(/^[a-zA-Z\s]*$/, {
message: 'Name can only contain letters and spaces'
})
name: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@MinLength(8)
@MaxLength(20)
@Matches(/((?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W]).{8,20})/, {
message: 'Password must contain at least one uppercase, one lowercase, one number and one special character'
})
password: string;
@IsInt()
@Min(18)
@Max(120)
@IsOptional()
age?: number;
@IsEnum(UserRole)
@IsOptional()
role?: UserRole = UserRole.USER;
@IsBoolean()
@IsOptional()
isActive?: boolean = true;
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(5)
@IsString({ each: true })
@IsOptional()
tags?: string[];
@ValidateNested()
@Type(() => AddressDto)
@IsOptional()
address?: AddressDto;
@IsUrl()
@IsOptional()
website?: string;
@IsPhoneNumber('US') // Specify country code
@IsOptional()
phoneNumber?: string;
@IsDateString()
@IsOptional()
birthDate?: string;
@IsNumber()
@IsPositive()
@IsOptional()
salary?: number;
@IsUUID()
@IsOptional()
companyId?: string;
}
export class UpdateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
@IsOptional()
name?: string;
@IsEmail()
@IsOptional()
email?: string;
@IsString()
@MinLength(8)
@MaxLength(20)
@IsOptional()
password?: string;
@IsInt()
@Min(18)
@Max(120)
@IsOptional()
age?: number;
}
Advanced Validation Examples:
1. Conditional Validation
typescript
import {
ValidateIf,
IsNotEmpty,
IsString
} from 'class-validator';
export class PaymentDto {
@IsString()
@IsNotEmpty()
paymentMethod: string;
@ValidateIf(o => o.paymentMethod === 'credit_card')
@IsString()
@IsNotEmpty()
cardNumber?: string;
@ValidateIf(o => o.paymentMethod === 'credit_card')
@IsString()
@IsNotEmpty()
expiryDate?: string;
@ValidateIf(o => o.paymentMethod === 'paypal')
@IsString()
@IsNotEmpty()
paypalEmail?: string;
}
2. Custom Validation Decorator
typescript
// decorators/match.decorator.ts
import {
registerDecorator,
ValidationOptions,
ValidationArguments
} from 'class-validator';
export function Match(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'match',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [property],
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${propertyName} must match ${relatedPropertyName}`;
},
},
});
};
}
// Usage
export class CreateUserDto {
@IsString()
password: string;
@IsString()
@Match('password', { message: 'Passwords do not match' })
confirmPassword: string;
}
3. Array Validation
typescript
export class CreateOrderDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
class OrderItemDto {
@IsUUID()
productId: string;
@IsInt()
@Min(1)
quantity: number;
@IsNumber()
@IsPositive()
price: number;
}
4. Date Validation
typescript
import { IsDate, MinDate, MaxDate } from 'class-validator';
export class EventDto {
@IsDate()
@Type(() => Date)
startDate: Date;
@IsDate()
@Type(() => Date)
@MinDate(new Date())
@MinDate((o: EventDto) => o.startDate)
endDate: Date;
}
4. Using Validation Pipe
Global Validation Pipe Configuration:
typescript
// main.ts
import { ValidationPipe, BadRequestException } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
// Validation settings
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
// Custom error handling
exceptionFactory: (errors) => {
const messages = errors.map(error => {
const constraints = error.constraints;
if (constraints) {
return {
field: error.property,
errors: Object.values(constraints),
};
}
return {
field: error.property,
errors: ['Validation failed'],
};
});
return new BadRequestException({
statusCode: 400,
message: 'Validation failed',
errors: messages,
timestamp: new Date().toISOString(),
});
},
}));
await app.listen(3000);
}
bootstrap();
Controller-Level Validation:
typescript
import {
Controller,
Post,
Body,
UsePipes,
ValidationPipe
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
create(@Body() createUserDto: CreateUserDto) {
// DTO is already validated and transformed
return createUserDto;
}
}
Parameter Validation:
typescript
import {
Controller,
Get,
Param,
Query,
ParseIntPipe,
DefaultValuePipe,
BadRequestException
} from '@nestjs/common';
@Controller('products')
export class ProductsController {
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query('minPrice', new ParseIntPipe({
optional: true,
exceptionFactory: () => new BadRequestException('minPrice must be a number')
})) minPrice?: number,
) {
return {
page,
limit,
minPrice,
products: []
};
}
}
5. Creating Custom Pipes
1. Parse Date Pipe
typescript
// pipes/parse-date.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string, metadata: ArgumentMetadata): Date {
if (!value) {
throw new BadRequestException('Date is required');
}
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException(`Invalid date: ${value}`);
}
return date;
}
}
// Usage
@Get('events')
getEvents(@Query('date', ParseDatePipe) date: Date) {
return { date };
}
2. Trim Strings Pipe
typescript
// pipes/trim.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata
} from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform {
private isObj(obj: any): boolean {
return typeof obj === 'object' && obj !== null;
}
private trim(values) {
Object.keys(values).forEach(key => {
if (key !== 'password') { // Don't trim passwords
if (this.isObj(values[key])) {
values[key] = this.trim(values[key]);
} else {
if (typeof values[key] === 'string') {
values[key] = values[key].trim();
}
}
}
});
return values;
}
transform(values: any, metadata: ArgumentMetadata) {
const { type } = metadata;
if (type === 'body') {
if (this.isObj(values)) {
return this.trim(values);
}
}
return values;
}
}
// Usage in controller
@Post()
@UsePipes(TrimPipe, ValidationPipe)
create(@Body() createUserDto: CreateUserDto) {
return createUserDto;
}
3. File Validation Pipe
typescript
// pipes/file-validation.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
interface FileOptions {
maxSize?: number; // in bytes
allowedTypes?: string[];
}
@Injectable()
export class FileValidationPipe implements PipeTransform {
constructor(private options: FileOptions = {}) {
this.options = {
maxSize: 5 * 1024 * 1024, // 5MB default
allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
...options,
};
}
transform(file: Express.Multer.File, metadata: ArgumentMetadata) {
if (!file) {
throw new BadRequestException('File is required');
}
// Check file size
if (file.size > this.options.maxSize) {
throw new BadRequestException(
`File too large. Maximum size is ${this.options.maxSize / 1024 / 1024}MB`,
);
}
// Check file type
if (!this.options.allowedTypes.includes(file.mimetype)) {
throw new BadRequestException(
`Invalid file type. Allowed types: ${this.options.allowedTypes.join(', ')}`,
);
}
return file;
}
}
// Usage
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(
@UploadedFile(FileValidationPipe) file: Express.Multer.File,
) {
return {
filename: file.originalname,
size: file.size,
mimetype: file.mimetype,
};
}
4. Query Parameter Validation Pipe
typescript
// pipes/sort.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
@Injectable()
export class SortPipe implements PipeTransform<string, string> {
private allowedFields = ['createdAt', 'updatedAt', 'name', 'price'];
private allowedOrders = ['asc', 'desc', 'ASC', 'DESC'];
transform(value: string, metadata: ArgumentMetadata): string {
if (!value) return 'createdAt:desc';
const [field, order] = value.split(':');
if (!field || !order) {
throw new BadRequestException(
'Invalid sort format. Use: field:order (e.g., createdAt:desc)',
);
}
if (!this.allowedFields.includes(field)) {
throw new BadRequestException(
`Invalid sort field. Allowed: ${this.allowedFields.join(', ')}`,
);
}
if (!this.allowedOrders.includes(order)) {
throw new BadRequestException(
`Invalid sort order. Allowed: ${this.allowedOrders.join(', ')}`,
);
}
return `${field}:${order.toLowerCase()}`;
}
}
// Usage
@Get()
findAll(@Query('sort', SortPipe) sort: string) {
const [field, order] = sort.split(':');
return { field, order };
}
5. Async Validation Pipe
typescript
// pipes/unique-email.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class UniqueEmailPipe implements PipeTransform {
constructor(private readonly usersService: UsersService) {}
async transform(value: any, metadata: ArgumentMetadata) {
if (metadata.type === 'body' && value.email) {
const userExists = await this.usersService.findByEmail(value.email);
if (userExists) {
throw new BadRequestException('Email already exists');
}
}
return value;
}
}
// Usage
@Post()
@UsePipes(UniqueEmailPipe, ValidationPipe)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
6. Built-in Exceptions
NestJS provides built-in HTTP exceptions:
typescript
import {
// 4xx Client Errors
BadRequestException, // 400
UnauthorizedException, // 401
ForbiddenException, // 403
NotFoundException, // 404
MethodNotAllowedException, // 405
NotAcceptableException, // 406
RequestTimeoutException, // 408
ConflictException, // 409
GoneException, // 410
PayloadTooLargeException, // 413
UnsupportedMediaTypeException, // 415
UnprocessableEntityException, // 422
TooManyRequestsException, // 429
// 5xx Server Errors
InternalServerErrorException, // 500
NotImplementedException, // 501
BadGatewayException, // 502
ServiceUnavailableException, // 503
GatewayTimeoutException, // 504
// HTTP Version
HttpVersionNotSupportedException, // 505
} from '@nestjs/common';
// Usage examples
throw new BadRequestException('Invalid input data');
throw new NotFoundException('User not found');
throw new UnauthorizedException('Invalid credentials');
throw new ForbiddenException('Insufficient permissions');
throw new ConflictException('Resource already exists');
throw new UnprocessableEntityException('Validation failed');
throw new InternalServerErrorException('Something went wrong');
Throwing Exceptions with Additional Data:
typescript
// Basic
throw new BadRequestException('Invalid email format');
// With object
throw new BadRequestException({
message: 'Validation failed',
errors: ['Email is required', 'Password too short'],
timestamp: new Date().toISOString(),
});
// With cause
try {
// some operation
} catch (error) {
throw new InternalServerErrorException('Database operation failed', {
cause: error,
description: 'Failed to connect to database',
});
}
7. Creating Custom Exceptions
1. Base Custom Exception
typescript
// exceptions/base.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class BaseException extends HttpException {
constructor(
message: string,
status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
code?: string,
metadata?: Record<string, any>,
) {
super(
{
message,
statusCode: status,
code: code || `ERR_${status}`,
timestamp: new Date().toISOString(),
...metadata,
},
status,
);
}
}
2. Business Logic Exceptions
typescript
// exceptions/business.exception.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class InsufficientFundsException extends BaseException {
constructor(amount: number, balance: number) {
super(
`Insufficient funds. Required: ${amount}, Available: ${balance}`,
HttpStatus.BAD_REQUEST,
'INSUFFICIENT_FUNDS',
{ amount, balance },
);
}
}
export class ProductOutOfStockException extends BaseException {
constructor(productId: string, available: number) {
super(
`Product ${productId} is out of stock. Available: ${available}`,
HttpStatus.CONFLICT,
'PRODUCT_OUT_OF_STOCK',
{ productId, available },
);
}
}
export class InvalidCouponException extends BaseException {
constructor(couponCode: string) {
super(
`Invalid coupon code: ${couponCode}`,
HttpStatus.BAD_REQUEST,
'INVALID_COUPON',
{ couponCode },
);
}
}
3. Database Exceptions
typescript
// exceptions/database.exception.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class DatabaseException extends BaseException {
constructor(message: string, error: any) {
super(
message,
HttpStatus.INTERNAL_SERVER_ERROR,
'DATABASE_ERROR',
{ originalError: error.message },
);
}
}
export class EntityNotFoundException extends BaseException {
constructor(entity: string, id: string | number) {
super(
`${entity} with ID ${id} not found`,
HttpStatus.NOT_FOUND,
'ENTITY_NOT_FOUND',
{ entity, id },
);
}
}
export class DuplicateEntryException extends BaseException {
constructor(entity: string, field: string, value: any) {
super(
`${entity} with ${field} = ${value} already exists`,
HttpStatus.CONFLICT,
'DUPLICATE_ENTRY',
{ entity, field, value },
);
}
}
4. Authentication & Authorization Exceptions
typescript
// exceptions/auth.exception.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class InvalidCredentialsException extends BaseException {
constructor() {
super(
'Invalid email or password',
HttpStatus.UNAUTHORIZED,
'INVALID_CREDENTIALS',
);
}
}
export class TokenExpiredException extends BaseException {
constructor() {
super(
'Authentication token has expired',
HttpStatus.UNAUTHORIZED,
'TOKEN_EXPIRED',
);
}
}
export class InsufficientPermissionsException extends BaseException {
constructor(requiredRole: string, userRole: string) {
super(
`Insufficient permissions. Required: ${requiredRole}, Your role: ${userRole}`,
HttpStatus.FORBIDDEN,
'INSUFFICIENT_PERMISSIONS',
{ requiredRole, userRole },
);
}
}
export class AccountLockedException extends BaseException {
constructor(lockUntil: Date) {
super(
`Account is locked until ${lockUntil.toISOString()}`,
HttpStatus.FORBIDDEN,
'ACCOUNT_LOCKED',
{ lockUntil },
);
}
}
5. External Service Exceptions
typescript
// exceptions/external-service.exception.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';
export class ExternalServiceException extends BaseException {
constructor(serviceName: string, error: any) {
super(
`${serviceName} service is unavailable`,
HttpStatus.SERVICE_UNAVAILABLE,
'EXTERNAL_SERVICE_ERROR',
{ serviceName, originalError: error.message },
);
}
}
export class PaymentGatewayException extends BaseException {
constructor(gateway: string, error: any) {
super(
`Payment failed via ${gateway}`,
HttpStatus.BAD_REQUEST,
'PAYMENT_FAILED',
{ gateway, errorCode: error.code, errorMessage: error.message },
);
}
}
Using Custom Exceptions:
typescript
// users.service.ts
import { Injectable } from '@nestjs/common';
import {
EntityNotFoundException,
DuplicateEntryException,
InsufficientPermissionsException,
} from './exceptions';
@Injectable()
export class UsersService {
async findOne(id: number) {
const user = await this.userRepository.findOne(id);
if (!user) {
throw new EntityNotFoundException('User', id);
}
return user;
}
async create(createUserDto: CreateUserDto) {
const existingUser = await this.userRepository.findByEmail(createUserDto.email);
if (existingUser) {
throw new DuplicateEntryException('User', 'email', createUserDto.email);
}
// Business logic
if (createUserDto.role === 'admin' && !this.isAdminCreating()) {
throw new InsufficientPermissionsException('admin', 'user');
}
return this.userRepository.create(createUserDto);
}
}
8. Exception Filters
1. Global Exception Filter
typescript
// filters/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let errorCode = 'INTERNAL_SERVER_ERROR';
let stack: string | undefined;
let validationErrors: any;
// Handle HttpException (NestJS built-in)
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (typeof exceptionResponse === 'object') {
message = (exceptionResponse as any).message || message;
errorCode = (exceptionResponse as any).code || errorCode;
validationErrors = (exceptionResponse as any).errors;
}
}
// Handle Validation Errors
else if (exception instanceof Error && 'errors' in (exception as any)) {
status = HttpStatus.BAD_REQUEST;
message = 'Validation failed';
errorCode = 'VALIDATION_ERROR';
validationErrors = (exception as any).errors;
}
// Log error
if (status >= 500) {
this.logger.error(
`${request.method} ${request.url} ${status} - ${message}`,
exception instanceof Error ? exception.stack : undefined,
);
} else {
this.logger.warn(
`${request.method} ${request.url} ${status} - ${message}`,
);
}
// In development, include stack trace
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
stack = exception.stack;
}
// Send response
response.status(status).json({
statusCode: status,
message,
errorCode,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
...(validationErrors && { errors: validationErrors }),
...(stack && { stack }),
});
}
}
// Register in main.ts
import { GlobalExceptionFilter } from './filters/global-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new GlobalExceptionFilter());
await app.listen(3000);
}
2. HTTP Exception Filter
typescript
// filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message,
...(typeof exceptionResponse === 'object' && exceptionResponse),
});
}
}
3. Validation Exception Filter
typescript
// filters/validation-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
let errors = [];
if (typeof exceptionResponse === 'object') {
const responseObj = exceptionResponse as any;
if (responseObj.message && Array.isArray(responseObj.message)) {
// Handle class-validator errors
errors = responseObj.message.map((error: any) => ({
field: error.property,
constraints: error.constraints,
}));
} else if (responseObj.errors) {
// Handle custom validation errors
errors = responseObj.errors;
}
}
response.status(status).json({
statusCode: status,
message: 'Validation failed',
errors,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
4. Database Exception Filter
typescript
// filters/database-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
} from '@nestjs/common';
import { QueryFailedError } from 'typeorm';
import { Request, Response } from 'express';
@Catch(QueryFailedError)
export class DatabaseExceptionFilter implements ExceptionFilter {
catch(exception: QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// Extract useful information from the error
const errorCode = (exception as any).code;
let message = 'Database error occurred';
let status = 500;
// Handle specific database errors
switch (errorCode) {
case '23505': // Unique violation
message = 'Resource already exists';
status = 409;
break;
case '23503': // Foreign key violation
message = 'Referenced resource not found';
status = 400;
break;
case '23502': // Not null violation
message = 'Required field is missing';
status = 400;
break;
}
response.status(status).json({
statusCode: status,
message,
errorCode,
timestamp: new Date().toISOString(),
path: request.url,
...(process.env.NODE_ENV === 'development' && {
detail: exception.message,
}),
});
}
}
5. Business Exception Filter
typescript
// filters/business-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BaseException } from '../exceptions/base.exception';
@Catch(BaseException)
export class BusinessExceptionFilter implements ExceptionFilter {
catch(exception: BaseException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response.status(status).json({
...(typeof exceptionResponse === 'object' ? exceptionResponse : {}),
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
});
}
}
Applying Multiple Filters:
typescript
// main.ts
import {
ValidationExceptionFilter,
DatabaseExceptionFilter,
BusinessExceptionFilter,
GlobalExceptionFilter,
} from './filters';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Apply multiple filters in order
app.useGlobalFilters(
new ValidationExceptionFilter(),
new DatabaseExceptionFilter(),
new BusinessExceptionFilter(),
new GlobalExceptionFilter(),
);
await app.listen(3000);
}
9. Global Error Handling
Complete Error Handling Setup:
typescript
// config/error-handling.config.ts
import {
ValidationPipe,
BadRequestException,
HttpStatus,
} from '@nestjs/common';
export const validationPipeConfig = new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
exceptionFactory: (errors) => {
const formattedErrors = errors.map((error) => ({
field: error.property,
errors: error.constraints ? Object.values(error.constraints) : [],
...(error.children && error.children.length > 0 && {
children: error.children,
}),
}));
return new BadRequestException({
statusCode: HttpStatus.BAD_REQUEST,
message: 'Validation failed',
errors: formattedErrors,
timestamp: new Date().toISOString(),
});
},
});
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { validationPipeConfig } from './config/error-handling.config';
import { GlobalExceptionFilter } from './filters/global-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
// Global pipes
app.useGlobalPipes(validationPipeConfig);
// Global filters
app.useGlobalFilters(new GlobalExceptionFilter());
// Global prefix
app.setGlobalPrefix('api/v1');
// Enable CORS
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
// Enable shutdown hooks
app.enableShutdownHooks();
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
}
Error Response Interceptor:
typescript
// interceptors/error-response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Request } from 'express';
@Injectable()
export class ErrorResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const requestId = request.headers['x-request-id'] || Date.now().toString();
return next.handle().pipe(
map((data) => ({
success: true,
data,
requestId,
timestamp: new Date().toISOString(),
})),
catchError((error) => {
const status =
error instanceof HttpException
? error.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const response = {
success: false,
statusCode: status,
message:
error.message ||
(status >= 500 ? 'Internal server error' : 'Bad request'),
requestId,
timestamp: new Date().toISOString(),
path: request.url,
};
// Add validation errors if present
if (error.response?.errors) {
response['errors'] = error.response.errors;
}
// Add error code if present
if (error.response?.code) {
response['code'] = error.response.code;
}
// In development, include stack trace
if (process.env.NODE_ENV === 'development' && error.stack) {
response['stack'] = error.stack;
}
return throwError(
() =>
new HttpException(response, status, {
cause: error,
}),
);
}),
);
}
}
// Apply globally in main.ts
app.useGlobalInterceptors(new ErrorResponseInterceptor());
Comments (0)
No comments yet. Be the first to comment!