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:

  1. Data Integrity: Prevent invalid data from entering your system

  2. Security: Protect against injection attacks

  3. User Experience: Provide clear error messages

  4. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?