1. ValidationPipe — 自動検証
# 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
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;
}
検証エラーの応答
// 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.カスタムパイプ
// 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;
}
}
内蔵パイプ
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. 例外フィルター
組み込みの HTTP 例外
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));
}
}
カスタム例外フィルター
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 {}
カスタム ビジネス例外
// 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. まとめ
- 検証パイプ: ホワイトリストを使用してグローバルに有効にし、変換します - DTO を自動的に検証します
- クラスバリデータ: @IsString、@IsEmail、@MinLength などのデコレータ
- カスタムパイプ: ハンドラーに到達する前にデータを変換/検証します。
- 例外フィルター: アプリ全体で均一にエラーをキャッチしてフォーマットします。
- 必ず投げる HTTP例外 一般的なエラーではなく特定のエラー
次の記事が展開されます パスポートとJWTによる認証。