Организация классов валидации

Архитектура классов валидации в прикладных JavaScript и TypeScript-приложениях строится вокруг принципа строгого разделения ответственности: данные описываются отдельно от бизнес-логики, а правила проверки выделяются в самостоятельные структуры. Такой подход снижает связанность компонентов и упрощает масштабирование системы.


Разделение доменной модели и слоёв валидации

В типичной архитектуре выделяются три уровня:

  • модель данных (domain model)
  • транспортные объекты (DTO)
  • слой валидации

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

Такой подход обеспечивает локализацию изменений: правила валидации пользователей не затрагивают авторизацию, и наоборот.

Внутри модуля классы также делятся по назначению:

  • входные DTO (request validation)
  • выходные DTO (response shaping)
  • внутренние DTO (service-level transfer objects)

Организация цепочек наследования

Наследование применяется для устранения дублирования полей между классами.

Базовый класс с общими полями:

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 множеством условных правил.


Использование validation groups для организации классов

Группы валидации позволяют применять разные правила к одному классу в зависимости от контекста.

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, поэтому применяется в системах с высокой степенью повторного использования моделей.


Изоляция 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

Дополнительное разделение:

  • public DTO (вход/выход API)
  • internal DTO (межсервисное взаимодействие)
  • external DTO (интеграции)

Переиспользование правил валидации через декораторы

При увеличении количества классов возникает необходимость переиспользования отдельных правил.

Создаются специализированные декораторы:

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;
}

Разделение ответственности:

  • class-transformer — преобразование типов
  • class-validator — проверка корректности

Централизованные и распределённые стратегии организации

Существует два подхода:

Централизованный:

Все DTO собраны в одном модуле dto/. Упрощает поиск, но снижает модульность.

Распределённый:

DTO находятся внутри каждого доменного модуля. Повышает автономность компонентов.

Распределённый подход чаще используется в крупных системах, где важна независимость модулей.


Версионирование классов валидации

При изменении API появляются новые версии DTO:

user/
  v1/
    create-user.dto.ts
  v2/
    create-user.dto.ts

Разделение версий предотвращает нарушение обратной совместимости и позволяет постепенно мигрировать потребителей API на новые схемы данных.