Chuyển đến nội dung chính

Lesson 8: Validation, Pipes and Exception Filters

class-validator, class-transformer, ValidationPipe, Custom Pipes. Built-in Exception Filters, Custom Exception Filters, HTTP Exceptions and error handling best practices.

💻 Programming — Lesson 8 Lesson 8: Validation, Pipes and Exceptions Filters

NestJS: From Basics to Advanced

Part 2: Providers, Dependency Injection & Data Layer

xdev.asia

1. ValidationPipe — Automatic validation

# Cài packages
npm install class-validator class-transformer
// main.ts — Bật global validation
import { ValidationPipe } from '@nestjs/common';

const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,           // Strip properties không có decorator
  forbidNonWhitelisted: true, // Throw error nếu có property lạ
  transform: true,           // Auto-transform types (string → number)
  transformOptions: {
    enableImplicitConversion: true,
  },
}));

DTO with Validation Decorators

import {
  IsString, IsEmail, IsOptional, IsEnum,
  MinLength, MaxLength, IsNumber, Min, Max,
  IsBoolean, IsArray, ValidateNested, IsUUID,
  IsNotEmpty, Matches, IsUrl, IsDateString,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(2)
  @MaxLength(100)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
    message: 'Password phải có ít nhất 1 chữ hoa, 1 chữ thường, 1 số',
  })
  password: string;

  @IsOptional()
  @IsEnum(['admin', 'user'])
  role?: string;

  @IsOptional()
  @IsUrl()
  avatar?: string;
}

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(200)
  title: string;

  @IsString()
  content: string;

  @IsOptional()
  @IsBoolean()
  published?: boolean;

  @IsOptional()
  @IsArray()
  @IsString({ each: true })  // Validate từng phần tử
  tags?: string[];
}

// Nested validation
export class CreateOrderDto {
  @IsUUID()
  productId: string;

  @IsNumber()
  @Min(1)
  @Max(100)
  quantity: number;

  @ValidateNested()
  @Type(() => AddressDto)
  shippingAddress: AddressDto;
}

export class AddressDto {
  @IsString() street: string;
  @IsString() city: string;
  @IsString() country: string;
}

Validation Error Response

// POST /users với body: { "name": "", "email": "invalid" }
// Response 400:
{
  "statusCode": 400,
  "message": [
    "name should not be empty",
    "name must be longer than or equal to 2 characters",
    "email must be an email"
  ],
  "error": "Bad Request"
}

2. Custom Pipes

// Parse UUID Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { validate as isUUID } from 'uuid';

@Injectable()
export class ParseUUIDPipe implements PipeTransform {
  transform(value: string) {
    if (!isUUID(value)) {
      throw new BadRequestException(`"${value}" is not a valid UUID`);
    }
    return value;
  }
}

// Sử dụng
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
  return this.service.findOne(id);
}
// Transform Pipe — trim và lowercase
@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any) {
    if (typeof value === 'string') {
      return value.trim().toLowerCase();
    }
    if (typeof value === 'object' && value !== null) {
      for (const key in value) {
        if (typeof value[key] === 'string') {
          value[key] = value[key].trim();
        }
      }
    }
    return value;
  }
}

Built-in Pipes

import { ParseIntPipe, ParseBoolPipe, ParseUUIDPipe, DefaultValuePipe } from '@nestjs/common';

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) { ... }

@Get()
findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('active', new DefaultValuePipe(true), ParseBoolPipe) active: boolean,
) { ... }

@Get(':id')
findByUUID(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) { ... }

3. Exception Filters

Built-in HTTP Exceptions

import {
  BadRequestException,     // 400
  UnauthorizedException,   // 401
  ForbiddenException,      // 403
  NotFoundException,       // 404
  ConflictException,       // 409
  UnprocessableEntityException, // 422
  InternalServerErrorException, // 500
} from '@nestjs/common';

@Injectable()
export class UsersService {
  async findOne(id: string): Promise<User> {
    const user = await this.repo.findOne({ where: { id } });
    if (!user) {
      throw new NotFoundException(`User với ID "${id}" không tồn tại`);
    }
    return user;
  }

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.repo.findOne({ where: { email: dto.email } });
    if (existing) {
      throw new ConflictException('Email đã được sử dụng');
    }
    return this.repo.save(this.repo.create(dto));
  }
}

Custom Exception Filter

import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()  // Bắt TẤT CẢ exceptions
export class AllExceptionsFilter implements ExceptionFilter {
  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 errors: any = null;

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const res = exception.getResponse();
      message = typeof res === 'string' ? res : (res as any).message;
      errors = typeof res === 'object' ? (res as any).errors : null;
    }

    response.status(status).json({
      success: false,
      statusCode: status,
      message,
      errors,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

// Đăng ký global
// main.ts
app.useGlobalFilters(new AllExceptionsFilter());

// Hoặc qua module
@Module({
  providers: [
    { provide: APP_FILTER, useClass: AllExceptionsFilter },
  ],
})
export class AppModule {}

Custom Business Exceptions

// Tạo exception riêng cho business logic
export class InsufficientBalanceException extends HttpException {
  constructor(balance: number, required: number) {
    super(
      {
        message: 'Số dư không đủ',
        balance,
        required,
        deficit: required - balance,
      },
      HttpStatus.UNPROCESSABLE_ENTITY,
    );
  }
}

// Sử dụng
if (user.balance < order.total) {
  throw new InsufficientBalanceException(user.balance, order.total);
}

4. Summary

  • ValidationPipe: Enable globally with whitelist, transform — automatically validate DTOs
  • class-validator: Decorators like @IsString, @IsEmail, @MinLength
  • Custom Pipes: Transform/validate data before reaching the handler
  • Exception Filters: Catch and format errors uniformly for the entire app
  • Always throw HttpException specific instead of generic Error

The next article will be deployed Authentication with Passport and JWT.