Kiểm soát dữ liệu đầu vào bằng DTO & Validation

Trong phát triển Backend, việc kiểm soát và xác thực dữ liệu do người dùng gửi lên (qua HTTP Request Body, Query hoặc Params) là bước tối quan trọng để bảo vệ hệ thống khỏi lỗi logic và các lỗ hổng bảo mật. Trong NestJS, việc này được thực hiện thông qua DTO (Data Transfer Object) kết hợp với bộ thư viện tự động validate. Tài liệu này hướng dẫn cách thiết lập DTO và cách sử dụng Claude Code để tự động hóa quy trình này.

1. DTO là gì?

DTO (Data Transfer Object - Đối tượng truyền dữ liệu) là một lớp (Class) xác định cấu trúc dữ liệu được gửi qua mạng.

Tại sao nên dùng DTO?

  • Xác thực dữ liệu (Validation): Đảm bảo dữ liệu gửi lên đúng định dạng (ví dụ: email phải đúng định dạng, tuổi phải là số dương).
  • Lọc dữ liệu thừa (Sanitization): Loại bỏ các trường dữ liệu lạ không được khai báo trong DTO (tránh lỗ hổng Mass Assignment - tin tặc tự ý gửi thêm trường role: 'admin' để chiếm quyền).
  • Đồng nhất kiểu dữ liệu: Giúp mã nguồn có kiểu dữ liệu rõ ràng (Type safety) cho TypeScript.

2. Thiết lập cơ chế Validate tự động

Để tự động hóa việc xác thực, chúng ta cần cài đặt hai thư viện bổ sung và kích hoạt ValidationPipe toàn cục trong tệp khởi động main.ts.

Cài đặt thư viện:

npm install class-validator class-transformer

Kích hoạt ValidationPipe toàn cục:

Yêu cầu Claude Code sửa file src/main.ts để bật chế độ lọc và chặn dữ liệu thừa:
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Bật ValidationPipe toàn cục
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // Tự động loại bỏ các trường không được định nghĩa trong DTO
    forbidNonWhitelisted: true, // Trả về lỗi 400 ngay lập tức nếu Client cố tình gửi trường thừa
    transform: true, // Tự động chuyển đổi kiểu dữ liệu (ví dụ: string '12' thành number 12)
  }));
  
  await app.listen(3000);
}
bootstrap();

3. Cách viết DTO với các Decorator phổ biến

Dưới đây là một CreateUserDto chứa đầy đủ các quy tắc xác thực thông dụng:
src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, MinLength, MaxLength, IsInt, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty({ message: 'Họ và tên không được để trống.' })
  fullName: string;

  @IsEmail({}, { message: 'Định dạng email không hợp lệ.' })
  email: string;

  @IsString()
  @MinLength(6, { message: 'Mật khẩu phải có tối thiểu 6 ký tự.' })
  @MaxLength(20, { message: 'Mật khẩu tối đa là 20 ký tự.' })
  password: string;

  @IsInt()
  @Min(18, { message: 'Bạn phải đủ 18 tuổi trở lên.' })
  @Max(100, { message: 'Tuổi không hợp lệ.' })
  @IsOptional() // Trường này không bắt buộc gửi lên
  age?: number;
}

Tạo DTO cập nhật (Update DTO) cực nhanh:

Thay vì viết lại toàn bộ các trường, bạn sử dụng PartialType để kế thừa và biến tất cả các trường thành tùy chọn (optional):
src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

4. Cách phối hợp viết DTO với Claude Code

Đối với người học không chuyên, việc nhớ các decorator kiểm tra dữ liệu của class-validator có thể khá phức tạp. Hãy để Claude Code xử lý việc này bằng các prompt mô tả nghiệp vụ của bạn:

Prompt mẫu yêu cầu Claude Code tạo DTO:

Hãy tạo CreatePostDto trong thư mục 'src/posts/dto' để validate dữ liệu gửi lên:
1. title: chuỗi ký tự, bắt buộc, độ dài từ 10 đến 100 ký tự.
2. content: chuỗi ký tự, bắt buộc, tối thiểu 20 ký tự.
3. categoryId: số nguyên, bắt buộc.
4. status: chuỗi ký tự, không bắt buộc, chỉ được phép nhận một trong các giá trị ['DRAFT', 'PUBLISHED'].
5. tags: mảng các chuỗi ký tự, không bắt buộc.

Đồng thời, tạo luôn file UpdatePostDto kế thừa từ CreatePostDto sử dụng PartialType.