Опирается на правила:
R-VLD-STD-1…R-VLD-STD-5иR-VLD-STD-X1…R-VLD-STD-X3из Validation Style Guide → раздел 2. Стандартные constraints.
Важно знать
- Required по умолчанию. В class-validator поле без
@IsOptional()считается обязательным — класс-трансформер выбросит ошибку, если значение отсутствует.@IsNotEmpty— строка не пустая и не из пробелов.@IsDefined— неundefined. Не взаимозаменяемы.@IsEmailвместо@Matches(emailRegex). Библиотека поддерживает RFC 5321/5322 и обновляется.@IsUUID,@IsUrl,@IsISO8601— форматы из коробки, не выдумывай regex.- Числа из query string:
transform: trueвValidationPipeприводит строку кnumber, но нужен@IsInt()или@IsNumber()для проверки.- Деньги — не
number. Плавающая точка теряет копейки. Принимать какstring, конвертировать вDecimal.js/BigIntв маппере.@IsOptional()— всегда первым декоратором на поле. Если значениеundefined, остальные декораторы пропускаются.
class-validator покрывает 80% типичных валидаций входных DTO. Стандартные декораторы читаются без объяснений: @IsEmail() понятен любому, @Matches(/^[^@]+@[^@]+$/) требует открывать regex. Свой велосипед ломает эту общую базу. Раскрытие раздела 2 гайда.
Required и optional
R-VLD-STD-1: обязательность — через отсутствие @IsOptional().
export class CreateCustomerRequest {
@IsUUID()
id: string; // required
@IsNotEmpty()
@MaxLength(100)
firstName: string; // required, непустое
@IsOptional()
@IsNotEmpty()
@MaxLength(100)
middleName?: string; // optional — если пришло, непустое
@IsEmail()
email: string; // required
@IsOptional()
@IsUrl()
website?: string; // optional url
}
Разница @IsNotEmpty vs @IsDefined:
| Декоратор | Что проверяет |
|---|---|
@IsDefined() | value !== undefined && value !== null |
@IsNotEmpty() | строка: value.trim().length > 0; массив: length > 0; объект: Object.keys(value).length > 0 |
@IsOptional() | пропустить остальные декораторы, если value === undefined или value === null |
Для строковых полей — @IsNotEmpty(). Для object-ссылок (вложенный объект) — достаточно @IsNotEmpty() или @ValidateNested плюс @Type.
Размеры — @Length, @Min, @Max, @ArrayMinSize
R-VLD-STD-2: правильный декоратор по типу.
export class OrderItemRequest {
@IsString()
@IsNotEmpty()
@Length(1, 64)
sku: string;
@IsInt()
@Min(1)
@Max(9999)
quantity: number;
@IsString()
@Matches(/^\d+\.\d{2}$/) // строка "100.00", конвертируется в Decimal в маппере
unitPrice: string;
}
export class CreateOrderRequest {
@IsUUID()
customerId: string;
@ValidateNested({ each: true })
@Type(() => OrderItemRequest)
@ArrayMinSize(1)
@ArrayMaxSize(100)
items: OrderItemRequest[];
}
Правила:
@Length(min, max)— длина строки. Не для чисел.@Min(n)/@Max(n)— целочисленные и float-числа (numberв TypeScript).@ArrayMinSize(n)/@ArrayMaxSize(n)— размер массива. Аналог@Sizeна коллекции в Java.@IsPositive(),@IsNegative()— короткая форма для знака. Эквивалент@Min(1)и@Max(-1).
Формат — @IsEmail, @IsUUID, @IsUrl, @IsISO8601
R-VLD-STD-3: форматные декораторы из коробки, не @Matches.
export class CustomerProfile {
@IsEmail()
@MaxLength(254)
email: string;
@IsUUID('4')
orderId: string;
@IsUrl({ protocols: ['https'], require_protocol: true })
callbackUrl: string;
@IsISO8601()
scheduledAt: string; // "2025-03-15T10:00:00Z"
@IsPhoneNumber('RU') // встроенный через libphonenumber
phone: string;
}
@Matches(emailRegex) вместо @IsEmail() — R-VLD-STD-X2, антипаттерн:
// ПЛОХО — велосипед
@Matches(/^[^@]+@[^@]+\.[^@]+$/)
email: string;
// ХОРОШО
@IsEmail()
email: string;
Самописный regex пропускает user@@host.com, user@host без домена первого уровня. @IsEmail() из class-validator использует validator.js с учётом RFC.
Для редких доменных форматов @Matches остаётся уместным:
@Matches(/^[A-Z]{3}-\d{6}$/, { message: 'Артикул в формате AAA-000000' })
articleNumber: string;
Если тот же формат встречается в 3+ полях проекта — это сигнал к custom constraint (см. Custom constraints).
Время — @IsISO8601, @MinDate, @MaxDate
R-VLD-STD-4: входные даты в DTO приходят строкой. Декоратор + @Type(() => Date) для рекурсивных проверок.
export class CreateContractRequest {
@IsISO8601()
validFrom: string; // "2025-03-15" или "2025-03-15T00:00:00Z"
@IsISO8601()
validUntil: string;
}
Если нужны проверки относительно текущей даты — @Type(() => Date) + @MinDate/@MaxDate:
export class BookingRequest {
@Type(() => Date)
@MinDate(new Date())
checkIn: Date;
@Type(() => Date)
@MinDate(new Date())
checkOut: Date;
}
Правило «checkIn < checkOut» — cross-field constraint на классе (см. Cross-field validation).
Тип-зависимая валидация и числа из query string
R-VLD-STD-5: transform: true в ValidationPipe конвертирует query-строки в числа, но нужен декоратор-проверка.
// controller
@Get('orders')
findAll(@Query() query: OrdersFilterQuery) { ... }
// DTO для query params
export class OrdersFilterQuery {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number; // "1" → 1
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number; // "20" → 20
@IsOptional()
@IsEnum(OrderStatus)
status?: OrderStatus;
}
Без @Type(() => Number) query-параметр остаётся строкой в runtime — @IsInt() упадёт. @Type нужен явно, даже с transform: true, на @Query()-объектах.
Деньги — не number:
// ПЛОХО — число 0.1 + 0.2 ≠ 0.3
unitPrice: number;
// ХОРОШО — строка, конвертируем в Decimal в маппере
@IsString()
@Matches(/^\d+(\.\d{1,2})?$/, { message: 'Цена в формате 100.00' })
unitPrice: string;
@IsOptional — всегда первым
R-VLD-STD-X1: порядок декораторов важен — @IsOptional() должен быть до остальных.
// ХОРОШО
@IsOptional()
@IsEmail()
alternateEmail?: string;
// ПЛОХО — если value=undefined, @IsEmail() упадёт раньше @IsOptional()
@IsEmail()
@IsOptional()
alternateEmail?: string;
class-validator выполняет декораторы снизу вверх (как в TypeScript). @IsOptional() в конце (в коде) означает выполнится первым в рантайме. Всегда пишем его первым в коде для читаемости и располагаем так, чтобы он срабатывал до остальных.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
@Matches(emailRegex) вместо @IsEmail() | R-VLD-STD-X2 | @IsEmail() |
Composite-декоратор (@NotEmptyMaxLength50) поверх стандартных | R-VLD-STD-X3 | Два явных: @IsNotEmpty() @MaxLength(50) |
Деньги как number в DTO | R-VLD-STD-5 | string + @Matches(/^\d+\.\d{2}$/), Decimal в маппере |
@IsInt() на query-params без @Type(() => Number) | R-VLD-STD-5 | @Type(() => Number) @IsInt() |
Дублирование required-проверки (@IsDefined + @IsNotEmpty) | R-VLD-STD-X1 | Один подходящий декоратор |
Куда дальше
- Validation → раздел 2. Стандартные constraints — нормативные формулировки
R-VLD-STD-*. - Custom constraints — когда стандартных не хватает.
- Где валидировать — где декораторы применяются.
- Cross-field validation — для правил между несколькими полями.
- OpenAPI и DTO в NestJS — code-first: DTO-класс как единственный источник правды.