Декоратори class-validator для DTO
Декоратори class-validator для DTO
🎯 Мета лекції
- Опанувати повний арсенал декораторів
class-validatorдля різних типів даних - Навчитися комбінувати декоратори для створення складних правил валідації
- Вивчити валідацію вкладених об'єктів та масивів через
@ValidateNested()та@Type() - Засвоїти умовну валідацію через
@ValidateIf()та@IsOptional() - Зрозуміти кастомізацію повідомлень помилок для зручності клієнтів API
- Практикувати створення типобезпечних DTO-класів для різних доменних сутностей
🔑 Ключові терміни
- Decorator (декоратор): функція TypeScript, що прикріплює метадані до класу, властивості або методу
- Validation Constraint (обмеження валідації): правило, що визначає допустимі значення для властивості
- Nested Validation (вкладена валідація): валідація об'єктів усередині об'єктів (рекурсивна перевірка)
- Conditional Validation (умовна валідація): валідація, що застосовується лише за певних умов
- Custom Message (кастомне повідомлення): персоналізоване повідомлення про помилку валідації
- Type Coercion (приведення типу): автоматичне перетворення значення з одного типу в інший
Базові декоратори типів
Бібліотека class-validator надає декоратори для валідації всіх примітивних JavaScript-типів. Ці декоратори перевіряють runtime-тип значення, забезпечуючи відповідність фактичних даних TypeScript-анотаціям.
Рядки: @IsString()
Перевіряє, що значення є рядком (typeof value === 'string'):
import { IsString, MinLength, MaxLength } from 'class-validator';
export class CreateArticleDto {
@IsString()
@MinLength(5, {
message: 'Title is too short. Minimum length is $constraint1 characters',
})
@MaxLength(200, {
message: 'Title is too long. Maximum length is $constraint1 characters',
})
title: string;
@IsString()
@MinLength(50)
@MaxLength(10000)
content: string;
}
Супутні декоратори для рядків:
| Декоратор | Опис | Приклад |
|---|---|---|
@Length(min, max) | Довжина у діапазоні | @Length(3, 20) |
@MinLength(min) | Мінімальна довжина | @MinLength(8) |
@MaxLength(max) | Максимальна довжина | @MaxLength(255) |
@Matches(pattern) | Відповідність регулярному виразу | @Matches(/^[a-zA-Z0-9]+$/) |
@IsNotEmpty() | Не порожній рядок | @IsNotEmpty() |
@IsString()), потім структуру та обмеження (@MinLength(), @Matches()).Числа: @IsNumber() та @IsInt()
import { IsInt, IsNumber, Min, Max, IsPositive } from 'class-validator';
export class CreateProductDto {
@IsString()
name: string;
@IsNumber({ maxDecimalPlaces: 2 }, {
message: 'Price must be a number with up to 2 decimal places',
})
@IsPositive()
@Max(1000000)
price: number; // Дозволяє десяткові дроби: 19.99
@IsInt()
@Min(0)
@Max(10000)
stockQuantity: number; // Лише цілі числа: 42
}
Супутні декоратори для чисел:
| Декоратор | Опис | Приклад |
|---|---|---|
@IsInt() | Ціле число | @IsInt() |
@IsNumber(options) | Число (з опціями для десяткових знаків) | @IsNumber({ maxDecimalPlaces: 2 }) |
@Min(value) | Мінімальне значення | @Min(0) |
@Max(value) | Максимальне значення | @Max(100) |
@IsPositive() | Додатне число (> 0) | @IsPositive() |
@IsNegative() | Від'ємне число (< 0) | @IsNegative() |
@IsDivisibleBy(num) | Кратне числу | @IsDivisibleBy(5) |
Булеві значення: @IsBoolean()
import { IsBoolean } from 'class-validator';
export class UpdateSettingsDto {
@IsBoolean()
emailNotifications: boolean;
@IsBoolean()
darkMode: boolean;
}
@IsBoolean() приймає лише true або false. Рядки "true" та "false"не проходять валідацію без опції transform: true у ValidationPipe. Для query string параметрів використовуйте ParseBoolPipe або увімкніть трансформацію.Спеціалізовані декоратори форматів
Email: @IsEmail()
Валідує електронну адресу згідно зі стандартами RFC:
import { IsEmail } from 'class-validator';
export class RegisterDto {
@IsEmail({}, {
message: 'Please provide a valid email address',
})
email: string;
}
Опції @IsEmail():
@IsEmail({
allow_display_name: false, // Дозволити "Name <email@example.com>"
require_display_name: false, // Вимагати формат з ім'ям
allow_utf8_local_part: true, // Дозволити Unicode символи
require_tld: true, // Вимагати домен верхнього рівня (.com, .org)
})
email: string;
URL: @IsUrl()
Валідує формат URL:
import { IsUrl } from 'class-validator';
export class CreateWebsiteDto {
@IsUrl({
protocols: ['https'], // Дозволити лише HTTPS
require_protocol: true,
require_valid_protocol: true,
}, {
message: 'Website must be a valid HTTPS URL',
})
websiteUrl: string;
}
UUID: @IsUUID()
Валідує формат UUID (альтернатива ParseUUIDPipe):
import { IsUUID } from 'class-validator';
export class GetUserDto {
@IsUUID('4', {
message: 'User ID must be a valid UUIDv4',
})
userId: string;
}
Підтримувані версії: '3', '4', '5', 'all'.
Дата та час: @IsDate()
Валідує, що значення є об'єктом Date:
import { IsDate, MinDate, MaxDate } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateEventDto {
@IsString()
title: string;
@Type(() => Date) // Перетворити рядок ISO 8601 → Date
@IsDate()
@MinDate(new Date())
startDate: Date; // Дата не може бути в минулому
@Type(() => Date)
@IsDate()
endDate: Date;
}
@IsDate() перевіряє instanceof Date, тому JSON-рядки (наприклад, "2026-09-04") не пройдуть валідацію без трансформації. Завжди використовуйте@Type(() => Date) з class-transformer для автоматичного перетворення рядків у об'єкти Date.Enum-валідація: @IsEnum()
Перевіряє, чи належить значення до визначеного TypeScript enum:
import { IsEnum } from 'class-validator';
export enum UserRole {
Admin = 'admin',
User = 'user',
Moderator = 'moderator',
}
export enum Priority {
Low = 1,
Medium = 2,
High = 3,
}
export class CreateTaskDto {
@IsString()
title: string;
@IsEnum(Priority, {
message: 'Priority must be 1 (Low), 2 (Medium), or 3 (High)',
})
priority: Priority;
@IsEnum(UserRole)
assignedRole: UserRole;
}
Запит:
{
"title": "Fix bug",
"priority": 2,
"assignedRole": "admin"
}
Якщо клієнт надішле "priority": 5 або "assignedRole": "superuser", ValidationPipe викине BadRequestException.
class-validator автоматично перетворює рядки у числа при увімкненій опції transform: true.Опціональні поля: @IsOptional()
За замовчуванням всі властивості DTO є обов'язковими. Декоратор @IsOptional() дозволяє пропустити валідацію, якщо значення є null або undefined:
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
export class UpdateUserDto {
@IsOptional()
@IsString()
@MinLength(2)
name?: string; // Може бути відсутнім
@IsOptional()
@IsInt()
@Min(18)
age?: number; // Може бути відсутнім
@IsOptional()
@IsEmail()
email?: string; // Може бути відсутнім
}
Поведінка:
// ✅ Валідний запит: оновлюємо лише ім'я
{
"name": "John Doe"
}
// ✅ Валідний запит: оновлюємо всі поля
{
"name": "John Doe",
"age": 30,
"email": "john@example.com"
}
// ❌ Невалідний запит: name занадто короткий
{
"name": "J"
}
// Помилка: "name must be longer than or equal to 2 characters"
@IsOptional() має бути першим декоратором у ланцюгу. Якщо його розмістити після інших декораторів, валідація може працювати некоректно.Валідація масивів: @IsArray() та пов'язані декоратори
Масиви потребують спеціальної валідації як для самої структури (чи є значення масивом), так і для елементів усередині масиву.
Базова валідація масиву
import { IsArray, ArrayMinSize, ArrayMaxSize, IsString } from 'class-validator';
export class CreatePostDto {
@IsString()
title: string;
@IsArray()
@ArrayMinSize(1, {
message: 'Post must have at least one tag',
})
@ArrayMaxSize(10, {
message: 'Post cannot have more than 10 tags',
})
@IsString({ each: true }) // Валідувати кожен елемент масиву
tags: string[];
}
Запит:
{
"title": "Introduction to NestJS",
"tags": ["typescript", "backend", "nodejs"]
}
Ключова опція each: true:
Без each: true декоратор @IsString() валідував би сам масив, а не його елементи. З each: true валідація застосовується до кожного елемента масиву окремо.
@IsArray()
@IsString() // Перевіряє typeof tags === 'string' (завжди false)
tags: string[];
// Результат: помилка "tags must be a string"
@IsArray()
@IsString({ each: true }) // Перевіряє typeof tags[i] === 'string'
tags: string[];
// Результат: кожен елемент валідується окремо
Масиви об'єктів: @ValidateNested()
Для валідації масивів складних об'єктів використовується комбінація @ValidateNested() та @Type():
import { IsArray, ValidateNested, IsString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';
// DTO для елемента масиву
export class OrderItemDto {
@IsString()
productId: string;
@IsInt()
@Min(1)
quantity: number;
@IsNumber({ maxDecimalPlaces: 2 })
@IsPositive()
price: number;
}
// Основний DTO
export class CreateOrderDto {
@IsString()
customerId: string;
@IsArray()
@ValidateNested({ each: true }) // Валідувати кожен об'єкт
@Type(() => OrderItemDto) // Перетворити plain objects → OrderItemDto
items: OrderItemDto[];
}
Запит:
{
"customerId": "user-123",
"items": [
{
"productId": "prod-1",
"quantity": 2,
"price": 19.99
},
{
"productId": "prod-2",
"quantity": 1,
"price": 49.50
}
]
}
Якщо будь-який елемент масиву items не відповідає OrderItemDto, ValidationPipe поверне детальні помилки:
{
"statusCode": 400,
"message": [
"items.0.quantity must be a positive number",
"items.1.price must be a number"
],
"error": "Bad Request"
}
@Type(() => OrderItemDto) є обов'язковим для вкладених об'єктів. Без нього class-transformer не зможе перетворити plain objects у екземпляри класів, і валідація декораторів OrderItemDto не спрацює.Додаткові декоратори для масивів
| Декоратор | Опис | Приклад |
|---|---|---|
@ArrayNotEmpty() | Масив не порожній | @ArrayNotEmpty() |
@ArrayMinSize(num) | Мінімальна довжина | @ArrayMinSize(1) |
@ArrayMaxSize(num) | Максимальна довжина | @ArrayMaxSize(100) |
@ArrayUnique() | Всі елементи унікальні | @ArrayUnique() |
@ArrayContains(values) | Містить певні значення | @ArrayContains(['required']) |
@ArrayNotContains(values) | Не містить певні значення | @ArrayNotContains(['forbidden']) |
Вкладені об'єкти: @ValidateNested()
Для валідації об'єктів усередині об'єктів використовується @ValidateNested():
import { ValidateNested, IsString, IsInt, IsEmail } from 'class-validator';
import { Type } from 'class-transformer';
// Вкладений об'єкт адреси
export class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
@Matches(/^\d{5}(-\d{4})?$/, {
message: 'Postal code must be in format 12345 or 12345-6789',
})
postalCode: string;
@IsString()
country: string;
}
// Основний DTO користувача
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto; // Вкладений об'єкт
}
Запит:
{
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"postalCode": "10001",
"country": "USA"
}
}
Валідація рекурсивно перевірить всі правила AddressDto. Повідомлення про помилки будуть вказувати вкладену структуру:
{
"statusCode": 400,
"message": [
"address.postalCode must match /^\\d{5}(-\\d{4})?$/ regular expression",
"address.country must be a string"
],
"error": "Bad Request"
}
CreateOrderDto може містити CustomerDto, AddressDto, масив OrderItemDto[] тощо.Умовна валідація: @ValidateIf()
Декоратор @ValidateIf() дозволяє застосовувати валідацію лише за певних умов:
import { IsString, IsEmail, IsEnum, ValidateIf } from 'class-validator';
export enum ContactMethod {
Email = 'email',
Phone = 'phone',
}
export class CreateContactDto {
@IsString()
name: string;
@IsEnum(ContactMethod)
preferredContactMethod: ContactMethod;
// Email обов'язковий лише якщо preferredContactMethod === 'email'
@ValidateIf(o => o.preferredContactMethod === ContactMethod.Email)
@IsEmail()
email?: string;
// Phone обов'язковий лише якщо preferredContactMethod === 'phone'
@ValidateIf(o => o.preferredContactMethod === ContactMethod.Phone)
@Matches(/^\+?[\d\s-]{10,}$/)
phone?: string;
}
Поведінка:
// ✅ Валідний: вибрано email, phone відсутній
{
"name": "John Doe",
"preferredContactMethod": "email",
"email": "john@example.com"
}
// ✅ Валідний: вибрано phone, email відсутній
{
"name": "Jane Smith",
"preferredContactMethod": "phone",
"phone": "+1-555-0100"
}
// ❌ Невалідний: вибрано email, але email відсутній
{
"name": "Alice",
"preferredContactMethod": "email"
}
// Помилка: "email must be an email"
Складні умови
@ValidateIf() приймає функцію (object, value) => boolean, що дозволяє створювати складну логіку:
export class UpdateProfileDto {
@IsOptional()
@IsString()
username?: string;
// Новий пароль обов'язковий лише якщо надано currentPassword
@ValidateIf((o, value) => o.currentPassword !== undefined || value !== undefined)
@IsString()
@MinLength(8)
newPassword?: string;
@ValidateIf(o => o.newPassword !== undefined)
@IsString()
currentPassword?: string;
}
@ValidateIf()пропускає всі наступні декоратори, якщо умова повертає false. Це відрізняється від @IsOptional(), який пропускає валідацію лише для undefined/null.Кастомізація повідомлень про помилки
Всі декоратори class-validator приймають другий аргумент для налаштування повідомлень про помилки:
import { IsString, MinLength, MaxLength, IsEmail, Matches } from 'class-validator';
export class RegisterDto {
@IsString({
message: 'Username must be a text value',
})
@MinLength(3, {
message: 'Username is too short. It must be at least $constraint1 characters long',
})
@MaxLength(20, {
message: 'Username is too long. Maximum length is $constraint1 characters',
})
@Matches(/^[a-zA-Z0-9_]+$/, {
message: 'Username can only contain letters, numbers, and underscores',
})
username: string;
@IsEmail({}, {
message: 'Please provide a valid email address',
})
email: string;
@IsString()
@MinLength(8, {
message: 'Password must be at least $constraint1 characters long',
})
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain at least one uppercase letter, one lowercase letter, and one number',
})
password: string;
}
Шаблонні змінні в повідомленнях
class-validator підтримує кілька шаблонних змінних для динамічних повідомлень:
| Змінна | Опис | Приклад |
|---|---|---|
$property | Назва властивості | username |
$value | Поточне значення | "ab" |
$constraint1 | Перший параметр декоратора | У @MinLength(8) → 8 |
$constraint2 | Другий параметр декоратора | У @Length(3, 20) → 3 та 20 |
$target | Назва класу DTO | RegisterDto |
Приклад використання:
@MinLength(8, {
message: 'Field "$property" with value "$value" is too short. Minimum is $constraint1 characters',
})
password: string;
// Запит: { "password": "123" }
// Помилка: 'Field "password" with value "123" is too short. Minimum is 8 characters'
Функціональні повідомлення
Для складної логіки повідомлень використовуйте функцію:
@MinLength(8, {
message: (args) => {
if (args.value === undefined) {
return `${args.property} is required`;
}
return `${args.property} must be at least ${args.constraints[0]} characters long, but you provided only ${args.value.length}`;
},
})
password: string;
"minLength" клієнти отримують чіткі інструкції, що саме потрібно виправити.Практичний приклад: повний DTO для реєстрації
Об'єднаємо всі вивчені декоратори в реалістичний DTO:
import {
IsString,
IsEmail,
IsEnum,
IsOptional,
IsDate,
ValidateNested,
MinLength,
MaxLength,
Matches,
Min,
Max,
} from 'class-validator';
import { Type } from 'class-transformer';
export enum Gender {
Male = 'male',
Female = 'female',
Other = 'other',
}
export class AddressDto {
@IsString()
@MinLength(5)
street: string;
@IsString()
@MinLength(2)
city: string;
@IsString()
@Matches(/^\d{5}$/, { message: 'Postal code must be 5 digits' })
postalCode: string;
@IsString()
country: string;
}
export class RegisterUserDto {
@IsString({ message: 'Username must be a string' })
@MinLength(3, { message: 'Username must be at least 3 characters' })
@MaxLength(20, { message: 'Username must not exceed 20 characters' })
@Matches(/^[a-zA-Z0-9_]+$/, {
message: 'Username can only contain letters, numbers, and underscores',
})
username: string;
@IsEmail({}, { message: 'Please provide a valid email address' })
email: string;
@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, {
message: 'Password must contain uppercase, lowercase, number, and special character',
})
password: string;
@IsString()
@MinLength(2)
@MaxLength(50)
firstName: string;
@IsString()
@MinLength(2)
@MaxLength(50)
lastName: string;
@Type(() => Date)
@IsDate()
@Max(new Date().getTime(), {
message: 'Birth date cannot be in the future',
})
birthDate: Date;
@IsEnum(Gender, {
message: 'Gender must be one of: male, female, other',
})
gender: Gender;
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
}
Цей DTO забезпечує комплексну валідацію з детальними повідомленнями про помилки для зручності клієнтів API.
Підсумок: категорії декораторів class-validator
Базові типи
@IsString(), @IsNumber(), @IsInt(), @IsBoolean(), @IsDate()Формати
@IsEmail(), @IsUrl(), @IsUUID(), @IsPhoneNumber(), @IsIP()Діапазони
@Min(), @Max(), @Length(), @MinLength(), @MaxLength(), @MinDate(), @MaxDate()Масиви
@IsArray(), @ArrayMinSize(), @ArrayMaxSize(), @ArrayUnique(), each: trueОб'єкти
@ValidateNested(), @Type(), @IsDefined(), @IsObject()Умови
@IsOptional(), @ValidateIf(), @ValidatePromise()У наступній лекції ми розглянемо створення власних валідаторів через клас ValidatorConstraint для складних бізнес-правил, що не покриваються стандартними декораторами.