Request Validation Documentation
This documentation explains the features and usage of Request Module: Located at src/common/request
Overview
Request validation uses NestJS’s built-in ValidationPipe with class-validator decorators to validate request body, query parameters, and path parameters.
Related Documents
- Message Documentation - For internationalization and error message translation
- Handling Error Documentation - For exception handling and response formatting
- Doc Documentation - For API documentation integration with DTOs
- File Upload Documentation - For file validation pipes
Table of Contents
- Overview
- Related Documents
- Request Module
- Usage
- DTO with Doc
- Extending DTOs
- Custom Validators
- Validation Pipes
- Error Message Mapping
- Error Message Translation
Request Module
The validation system is configured globally in RequestModule:
new ValidationPipe({
transform: true,
skipMissingProperties: false,
skipNullProperties: false,
skipUndefinedProperties: false,
forbidUnknownValues: false,
whitelist: true,
forbidNonWhitelisted: true,
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
transformOptions: {
excludeExtraneousValues: false,
},
validationError: {
target: false,
value: true,
},
errorHttpStatusCode:
HttpStatus.UNPROCESSABLE_ENTITY,
exceptionFactory: async (
errors: ValidationError[]
) => new RequestValidationException(errors),
})Processing flow:
Request received
↓
ValidationPipe validates DTO
↓
Valid? → Continue to controller
↓ No
RequestValidationException thrown
↓
AppValidationFilter catches exception
↓
MessageService formats errors with i18n
↓
Standardized error response (HTTP 422)Usage
Request Body Validation
Apply DTO as type parameter in @Body() decorator:
@Controller('users')
export class UserController {
@Post()
create(@Body() body: CreateUserDto) {
// body is validated and transformed
return this.userService.create(body);
}
}DTO example:
export class CreateUserDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(8)
@MaxLength(50)
password: string;
@IsString()
@IsNotEmpty()
name: string;
}Query Parameters Validation
@Controller('users')
export class UserController {
@Get()
list(@Query() query: UserListDto) {
return this.userService.findAll(query);
}
}DTO example:
export class UserListDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@Min(1)
page?: number = 1;
@IsOptional()
@IsNumber()
@Type(() => Number)
@Min(10)
@Max(100)
limit?: number = 20;
}Path Parameters Validation
@Controller('users')
export class UserController {
@Get(':userId')
findOne(@Param() param: UserParamDto) {
return this.userService.findById(param.userId);
}
}DTO example:
export class UserParamDto {
@IsMongoId()
@IsNotEmpty()
userId: string;
}DTO with Doc
Combine class-validator decorators with @ApiPropertyfrom @nestjs/swagger :
import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { faker } from '@faker-js/faker';
export class CreateUserDto {
@ApiProperty({
description: 'User email address',
example: faker.internet.email(),
required: true,
})
@IsEmail()
@IsNotEmpty()
email: string;
@ApiProperty({
description: 'User password',
example: `${faker.string.alphanumeric(5).toLowerCase()}${faker.string.alphanumeric(5).toUpperCase()}@@!123`,
required: true,
minLength: 8,
maxLength: 50,
})
@IsString()
@IsNotEmpty()
@MinLength(8)
@MaxLength(50)
password: string;
@ApiProperty({
description: 'User full name',
example: faker.person.fullName(),
required: true,
})
@IsString()
@IsNotEmpty()
name: string;
}See Doc Documentation for complete guide with API documentation.
Extending DTOs
Use type helpers from @nestjs/swagger to maintain @ApiProperty validity when extending DTOs.
See @nestjs/swagger documentation for details.
Direct
export class UpdateUserDto extends CreateUserDto {
@ApiProperty({
description: 'User status',
example: 'active',
})
@IsString()
@IsOptional()
status?: string;
}PartialType
Makes all properties optional:
import { PartialType } from '@nestjs/swagger';
export class UpdateUserDto extends PartialType(CreateUserDto) {
}OmitType
Excludes specific properties:
import { OmitType } from '@nestjs/swagger';
export class UpdateUserDto extends OmitType(CreateUserDto, ['password'] as const) {
}IntersectionType
Combines multiple DTOs:
export class UserWithProfileDto extends IntersectionType(
CreateUserDto,
ProfileDto
) {
}Custom Validators
Available Custom Validators
Located in src/common/request/validations/*:
IsCustomEmail Enhanced email validation with detailed error messages:
export class CreateUserDto {
@IsCustomEmail()
@IsNotEmpty()
email: string;
}IsPassword Strong password validation:
export class ChangePasswordDto {
@IsPassword()
@IsNotEmpty()
@MinLength(8)
@MaxLength(50)
newPassword: string;
}IsAfterNow Validates date is after current time:
export class CreateEventDto {
@IsAfterNow()
@IsNotEmpty()
startDate: Date;
}GreaterThanOtherProperty Validates field is greater than another field:
export class CreateRangeDto {
@IsNumber()
minValue: number;
@GreaterThanOtherProperty('minValue')
@IsNumber()
maxValue: number;
}GreaterThanEqualOtherProperty Validates field is greater than or equal to another field:
export class CreateRangeDto {
@IsNumber()
minValue: number;
@GreaterThanEqualOtherProperty('minValue')
@IsNumber()
maxValue: number;
}LessThanOtherProperty Validates field is less than another field:
export class CreateDiscountDto {
@IsNumber()
maxDiscount: number;
@LessThanOtherProperty('maxDiscount')
@IsNumber()
minDiscount: number;
}LessThanEqualOtherProperty Validates field is less than or equal to another field:
export class CreateDiscountDto {
@IsNumber()
maxDiscount: number;
@LessThanEqualOtherProperty('maxDiscount')
@IsNumber()
minDiscount: number;
}Creating Custom Validator
For module-specific validators, create in module’s /validations folder. For global validators, add to
src/common/request/validations:
@ValidatorConstraint({ async: false })
@Injectable()
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
validate(value: string, args: ValidationArguments): boolean {
// Validation logic
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(value);
}
defaultMessage(args: ValidationArguments): string {
return 'request.error.passwordWeak';
}
}
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return function(object: unknown, propertyName: string): void {
registerDecorator({
name: 'IsStrongPassword',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsStrongPasswordConstraint,
});
};
}Register in module:
@Module({
providers: [IsStrongPasswordConstraint],
})
export class RequestModule {
}Validation Pipes
Pipes validate single fields for body, param, or query. For multiple fields, use DTO with class-validator.
RequestRequiredPipe Validates required parameters:
@Controller('users')
export class UserController {
@Get(':userId')
findOne(@Param('userId', RequestRequiredPipe) userId: string) {
return this.userService.findById(userId);
}
}RequestParseObjectIdPipe Validates MongoDB ObjectId:
@Get(':userId')
findOne(@Param('userId', RequestParseObjectIdPipe)
userId: string
)
{
return this.userService.findById(userId);
}File validation pipes See File Upload for file extension and Csv validation pipes.
Error Message Mapping
When validation fails, MessageService processes errors through setValidationMessage():
Process:
- Extract constraint keys from
ValidationErrorusingextractConstraints() - Handle nested validation errors by traversing children with
processNestedValidationError() - Reconstruct full property path for nested objects (e.g.,
address.street) - Create localized message for each constraint with fallback mechanism
- Format into
IMessageValidationError[]
Implementation (from MessageService):
setValidationMessage(
errors
:
ValidationError[],
options ? : IMessageErrorOptions
):
IMessageValidationError[]
{
const messages: IMessageValidationError[] = [];
for (const error of errors) {
let property = error.property;
// Extract constraints from current error
const constraints: string[] = this.extractConstraints(error);
// Handle nested errors if no direct constraints found
if (constraints.length === 0) {
const nestedResult = this.processNestedValidationError(error);
property = nestedResult.property; // Full path: address.street
constraints.push(...nestedResult.constraints);
}
// Create localized message for each constraint
for (const constraint of constraints) {
messages.push(
this.createValidationMessage(
constraint,
error.constraints[constraint],
error.value,
property,
options
)
);
}
}
return messages;
}Message Resolution Strategy:
- Primary: Tries to resolve from
request.error.{constraint}path - Fallback: If translation not found (message equals path), uses raw message from class-validator
Error structure:
interface IMessageValidationError {
key: string; // Constraint name (e.g., 'isEmail')
property: string; // Property path (e.g., 'user.email')
message: string; // Localized message
}Error Message Translation
Error messages are translated using nestjs-i18n through Message System.
Message path pattern: request.error.{constraintName}
Example message file (en/request.json):
{
"error": {
"isEmail": "{property} must be a valid email address",
"isNotEmpty": "{property} is required",
"minLength": "{property} must be at least {min} characters",
"isPassword": "{property} must contain uppercase, lowercase, number and special character"
}
}Custom validator message (from IsCustomEmailConstraint):
defaultMessage(validationArguments ? : ValidationArguments)
:
string
{
if (!validationArguments?.value) {
return 'request.error.email.required';
}
const validationResult = this.helperService.checkEmail(
validationArguments.value
);
return validationResult.messagePath ?? 'request.error.email.invalid';
}Final response (handled by AppValidationFilter):
{
"statusCode": 422,
"message": "Validation error",
"errors": [
{
"key": "isEmail",
"property": "email",
"message": "email must be a valid email address"
},
{
"key": "minLength",
"property": "password",
"message": "password must be at least 8 characters"
}
],
"metadata": {
"language": "en",
"timestamp": 1660190937231,
"timezone": "Asia/Jakarta",
"path": "/api/v1/users",
"version": "1",
"repoVersion": "1.0.0",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"correlationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}See Handling Error for complete error handling flow.