Архитектура классов валидации в прикладных JavaScript и TypeScript-приложениях строится вокруг принципа строгого разделения ответственности: данные описываются отдельно от бизнес-логики, а правила проверки выделяются в самостоятельные структуры. Такой подход снижает связанность компонентов и упрощает масштабирование системы.
В типичной архитектуре выделяются три уровня:
DTO-классы становятся основным местом применения декораторов class-validator, поскольку они описывают входные данные, поступающие из внешних источников: HTTP-запросов, очередей сообщений или CLI.
Пример базового DTO:
import { IsString, IsInt, MinLength } fr om "class-validator";
export class CreateUserDto {
@IsString()
@MinLength(3)
username: string;
@IsString()
password: string;
@IsInt()
age: number;
}
Модель домена при этом остаётся свободной от валидационных аннотаций:
export class User {
id: number;
username: string;
passwordHash: string;
age: number;
}
Такое разделение позволяет изменять правила валидации без влияния на бизнес-логику.
Классы валидации логически группируются по доменным модулям. Каждый модуль содержит собственные DTO, относящиеся к конкретной области системы.
Пример структуры:
src/
users/
dto/
create-user.dto.ts
update-user.dto.ts
user.entity.ts
auth/
dto/
login.dto.ts
refresh-token.dto.ts
Такой подход обеспечивает локализацию изменений: правила валидации пользователей не затрагивают авторизацию, и наоборот.
Внутри модуля классы также делятся по назначению:
Наследование применяется для устранения дублирования полей между классами.
Базовый класс с общими полями:
import { IsEmail, IsString } from "class-validator";
export class BaseUserDto {
@IsEmail()
email: string;
@IsString()
username: string;
}
Расширение для создания пользователя:
import { MinLength } from "class-validator";
import { BaseUserDto } from "./base-user.dto";
export class CreateUserDto extends BaseUserDto {
@MinLength(8)
password: string;
}
Расширение для обновления пользователя:
import { IsOptional, MinLength } from "class-validator";
import { BaseUserDto } from "./base-user.dto";
export class UpdateUserDto extends BaseUserDto {
@IsOptional()
@MinLength(8)
password?: string;
}
Использование наследования особенно эффективно при наличии устойчивых доменных сущностей с повторяющимися полями.
При усложнении доменной модели наследование заменяется композицией. Это позволяет переиспользовать фрагменты валидации без жёсткой иерархии.
Пример вложенного объекта:
import { IsString, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
Использование в основном DTO:
export class CreateUserDto {
@IsString()
username: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
Композиция повышает переиспользуемость и снижает связность классов.
В реальных системах правила проверки отличаются в зависимости от сценария использования одного и того же поля.
Для этого используются отдельные классы:
export class StrictUserDto {
@IsString()
@MinLength(5)
username: string;
}
export class RelaxedUserDto {
@IsString()
username: string;
}
Такое разделение предотвращает перегрузку одного DTO множеством условных правил.
Группы валидации позволяют применять разные правила к одному классу в зависимости от контекста.
import { IsString, MinLength } from "class-validator";
export class UserDto {
@IsString({ groups: ["create", "update"] })
username: string;
@MinLength(8, { groups: ["create"] })
password: string;
}
Применение групп:
import { validate } from "class-validator";
validate(dto, { groups: ["create"] });
validate(dto, { groups: ["update"] });
Использование групп уменьшает количество классов, но увеличивает сложность внутри одного DTO, поэтому применяется в системах с высокой степенью повторного использования моделей.
Классы валидации не должны содержать вычислений, обращений к базе данных или трансформаций данных. Их задача ограничивается описанием правил проверки.
Пример недопустимой практики:
export class BadDto {
@IsString()
username: string;
async checkInDatabase() {
// нарушение архитектуры
}
}
Корректный подход:
export class GoodDto {
@IsString()
username: string;
}
Логика проверки переносится в сервисный слой, оставляя DTO чистыми.
При работе с комплексными объектами важно соблюдать единый стиль описания вложенности.
export class ProfileDto {
@IsString()
bio: string;
}
export class CreateUserDto {
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
Глубина вложенности должна контролироваться, чтобы избежать избыточной сложности. При увеличении количества уровней целесообразно выделять отдельные DTO для каждого поддерева данных.
Базовые классы позволяют централизовать повторяющиеся правила:
import { IsUUID } from "class-validator";
export class BaseEntityDto {
@IsUUID()
id: string;
}
Расширение:
export class UserResponseDto extends BaseEntityDto {
username: string;
}
При необходимости базовые классы могут делиться на:
export class TimestampDto {
createdAt: Date;
updatedAt: Date;
}
При росте проекта структура каталогов становится критическим фактором поддерживаемости.
Рекомендуемая организация:
dto/
base/
base-entity.dto.ts
timestamp.dto.ts
user/
create-user.dto.ts
update-user.dto.ts
user-response.dto.ts
auth/
login.dto.ts
Дополнительное разделение:
При увеличении количества классов возникает необходимость переиспользования отдельных правил.
Создаются специализированные декораторы:
import { IsString, MinLength } from "class-validator";
export function IsStrongPassword() {
return function (object: Object, propertyName: string) {
IsString()(object, propertyName);
MinLength(8)(object, propertyName);
};
}
Использование:
export class CreateUserDto {
@IsStrongPassword()
password: string;
}
Такой подход уменьшает дублирование и стандартизирует правила.
Валидация часто сопровождается трансформацией входных данных через class-transformer.
import { Type } from "class-transformer";
import { IsNumber } from "class-validator";
export class QueryDto {
@Type(() => Number)
@IsNumber()
lim it: number;
}
Разделение ответственности:
Существует два подхода:
Централизованный:
Все DTO собраны в одном модуле dto/. Упрощает поиск, но
снижает модульность.
Распределённый:
DTO находятся внутри каждого доменного модуля. Повышает автономность компонентов.
Распределённый подход чаще используется в крупных системах, где важна независимость модулей.
При изменении API появляются новые версии DTO:
user/
v1/
create-user.dto.ts
v2/
create-user.dto.ts
Разделение версий предотвращает нарушение обратной совместимости и позволяет постепенно мигрировать потребителей API на новые схемы данных.