Declare Validation Where You Define the Shape
class-validator — DTOs With Rules
class-validator decorators sit next to each DTO field, declaring exactly what's allowed. ValidationPipe enforces them.
What you'll learn
- Apply common decorators (@IsString, @IsEmail, @MinLength, @IsOptional)
- Validate nested objects with @ValidateNested and @Type
- Customize error messages per rule
DTOs (Data Transfer Objects) are plain classes that describe the shape of incoming data. class-validator decorators turn those shapes into rules.
The Decorator Toolkit
Most validation is just stacking the right decorators on each field.
import {
IsEmail, IsString, IsInt, MinLength, MaxLength,
IsOptional, IsArray, ArrayMinSize, Matches, Min, Max,
} from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@MaxLength(72)
@Matches(/[A-Z]/, { message: 'password must contain an uppercase letter' })
password: string;
@IsInt()
@Min(13)
@Max(120)
age: number;
@IsOptional()
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
tags?: string[];
} A few patterns worth noting:
each: trueruns the rule against each item in an array.@IsOptional()short-circuits the rest of the chain when the field isundefinedornull— the natural way to make a field optional.@Matches(regex)is your escape hatch when no built-in fits.
Nested Objects
A DTO can contain another DTO. You need two decorators to tell the validator
to recurse: @ValidateNested (run the rules) and @Type (build it as a
class instance, not a plain object).
import { Type } from 'class-transformer';
import { ValidateNested, IsString } from 'class-validator';
class Address {
@IsString() street: string;
@IsString() city: string;
}
export class CreateOrderDto {
@ValidateNested()
@Type(() => Address)
shipping: Address;
} Without @Type, shipping stays a plain object and its decorators never
fire. It’s the single most common gotcha — add @Type whenever you have a
nested DTO.
Custom Error Messages
Every rule takes a message option. It can be a string or a function that
gets the validation arguments.
export class LoginDto {
@IsEmail({}, { message: 'please use a valid email' })
email: string;
@MinLength(8, {
message: (args) =>
`password must be at least ${args.constraints[0]} characters (got ${args.value.length})`,
})
password: string;
} When ValidationPipe reports a failure, your message shows up directly in
the message array of the response.
Reusing Rules With Groups
Sometimes the same DTO is used for create and update, with different requirements. Groups let one rule fire only in one context.
export class UpsertUserDto {
@IsEmail({}, { groups: ['create'] }) // required on create only
@IsOptional({ groups: ['update'] })
email?: string;
} Pass the group when constructing the pipe (new ValidationPipe({ groups: ['create'] })) and the right subset fires.
class-validator handles inbound shape. The next lesson covers class-transformer, which handles outbound shape.
class-transformer — Shape Your Output →