В прикладных API слой передачи данных (DTO — Data Transfer Object) используется как формальный контракт между клиентом и сервером. DTO фиксирует структуру входных и выходных данных, позволяя отделить доменную модель от внешнего интерфейса.
В JavaScript/TypeScript-проектах DTO особенно часто применяются в связке с фреймворками наподобие NestJS, где входящие HTTP-запросы преобразуются в экземпляры классов. Это открывает возможность применять декларативную валидацию через декораторы, не смешивая бизнес-логику и проверку данных.
Библиотека Class-validator предоставляет механизм валидации объектов на основе декораторов классов. Основная идея заключается в том, что правила описываются прямо в DTO-классе, а проверка выполняется отдельно.
Ключевые особенности подхода:
DTO представляет собой обычный класс, где каждое поле снабжается набором декораторов:
import { IsString, IsInt, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(3)
@MaxLength(20)
username: string;
@IsInt()
age: number;
}
Каждый декоратор добавляет метаданные, которые затем используются валидатором для проверки объекта.
Основные типы проверок:
@IsString, @IsNumber,
@IsBoolean);@MinLength,
@MaxLength);@Min, @Max);@IsEmail, @IsUUID,
@IsDate);@IsOptional,
@IsNotEmpty).Валидация выполняется через функцию validate или
validateOrReject:
import { validate } from 'class-validator';
const dto = new CreateUserDto();
dto.username = 'ab';
dto.age = 25;
validate(dto).then(errors => {
console.log(errors);
});
Если объект не соответствует правилам, возвращается массив ошибок, каждая из которых содержит:
При использовании validateOrReject вместо массива ошибок
выбрасывается исключение, что удобно для серверной обработки.
В реальных приложениях DTO формируется из тела запроса. Важно, что входные данные приходят в виде plain object, поэтому перед валидацией часто требуется преобразование в экземпляр класса:
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
const body = {
username: 'alex',
age: 30
};
const dto = plainToInstance(CreateUserDto, body);
validate(dto).then(errors => {
console.log(errors);
});
Без преобразования декораторы не будут корректно работать, так как отсутствуют метаданные класса.
DTO часто содержит вложенные структуры. Для их проверки используется
комбинация @ValidateNested и @Type:
import { Type } from 'class-transformer';
import { ValidateNested, IsString } from 'class-validator';
class ProfileDto {
@IsString()
city: string;
}
class CreateUserDto {
@IsString()
username: string;
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
Без @Type вложенный объект не будет преобразован в
экземпляр класса, и валидация не выполнится.
Для массивов объектов применяется комбинация @IsArray и
@ValidateNested({ each: true }):
import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class RoleDto {
name: string;
}
class UserDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => RoleDto)
roles: RoleDto[];
}
Параметр each: true включает проверку каждого элемента
массива отдельно.
Группы позволяют включать или отключать проверки в зависимости от контекста:
import { IsString } from 'class-validator';
class UpdateUserDto {
@IsString({ groups: ['update'] })
username: string;
}
При вызове валидации указывается группа:
validate(dto, { groups: ['update'] });
Механизм групп используется при различии сценариев:
Для частичных обновлений применяются @IsOptional:
import { IsOptional, IsString } from 'class-validator';
class UpdateUserDto {
@IsOptional()
@IsString()
username?: string;
}
Если поле отсутствует, остальные правила не применяются.
Валидация DTO часто используется совместно с фильтрацией лишних полей:
validate(dto, {
whitelist: true,
forbidNonWhitelisted: true
});
Поведение:
whitelist: true — удаляет поля, не описанные в
DTO;forbidNonWhitelisted: true — выбрасывает ошибку при
наличии лишних полей.Этот механизм защищает API от неожиданных входных данных.
Когда встроенных проверок недостаточно, создаются собственные правила
через ValidatorConstraint:
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments
} from 'class-validator';
@ValidatorConstraint({ name: 'isEven', async: false })
export class IsEvenConstraint implements ValidatorConstraintInterface {
validate(value: number) {
return value % 2 === 0;
}
defaultMessage(args: ValidationArguments) {
return `${args.property} must be an even number`;
}
}
Использование в DTO:
import { Validate } from 'class-validator';
class NumberDto {
@Validate(IsEvenConstraint)
value: number;
}
Кастомные валидаторы позволяют внедрять бизнес-правила напрямую в слой DTO.
Некоторые проверки требуют обращения к внешним системам (например, проверка уникальности пользователя). Для этого валидатор может быть асинхронным:
@ValidatorConstraint({ async: true })
class IsUserExistsConstraint {
async validate(username: string) {
const user = await database.findUser(username);
return !user;
}
}
Асинхронные проверки увеличивают гибкость, но требуют аккуратного управления производительностью.
Результат валидации представляет собой дерево ошибок:
property);constraints);children).Пример обработки:
validate(dto).then(errors => {
errors.forEach(error => {
console.log(error.property);
console.log(error.constraints);
});
});
При вложенных DTO структура ошибок становится иерархической.
В реальных DTO декораторы часто комбинируются:
class ProductDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsNumber()
@Min(0)
price: number;
@IsOptional()
@IsString()
description?: string;
}
Порядок декораторов не влияет на результат, так как они накапливают метаданные.
На практике часто возникают следующие проблемы:
class-transformer при работе с plain
objects;@Type для вложенных структур;whitelist, приводящее к “грязным”
данным;any в DTO;Каждая из этих ошибок приводит к ослаблению гарантий типизации и валидации.
При большом количестве DTO и сложных вложенных структурах важно учитывать:
Оптимизация обычно достигается за счёт ограничения глубины DTO и минимизации внешних вызовов в кастомных валидаторах.