Структура проекта для работы с валидацией

При использовании class-validator ключевым фактором становится не столько набор декораторов, сколько архитектура проекта, в которой валидация занимает чётко определённое место. Ошибкой является смешивание бизнес-логики, описания данных и правил проверки в одном слое. Это приводит к дублированию правил, усложнению тестирования и невозможности масштабирования.

Рациональная структура проекта предполагает разделение на несколько уровней: транспортный слой (HTTP/CLI/GraphQL), слой DTO (Data Transfer Objects), слой доменной модели и слой бизнес-логики. Валидация в таком подходе концентрируется преимущественно в DTO и частично на границах домена.


Роль DTO в архитектуре с class-validator

DTO выступают контрактом входных и выходных данных. В контексте class-validator именно DTO становятся основным носителем правил валидации через декораторы.

Типичный DTO:

import { IsString, IsInt, MinLength, IsOptional } fr om 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  username: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsInt()
  age?: number;
}

DTO не должен содержать бизнес-логики. Его задача — описать форму данных и ограничения на уровне структуры.


Разделение ответственности между слоями

Чёткое разделение слоёв позволяет избежать смешивания задач:

  • DTO — структура данных и первичная валидация
  • сервисы — бизнес-логика
  • контроллеры — маршрутизация и преобразование входных данных
  • сущности — доменная модель

Валидация через class-validator должна завершаться до попадания данных в сервисный слой. Это означает, что сервисы работают уже с гарантированно корректными объектами.


Структура каталогов проекта

При проектировании проекта с использованием class-validator часто применяется следующая структура:

src/
  modules/
    user/
      dto/
        create-user.dto.ts
        update-user.dto.ts
      entities/
        user.entity.ts
      user.service.ts
      user.controller.ts
      user.module.ts
  shared/
    validators/
    pipes/
    decorators/
  common/
    errors/
    interceptors/

Такое разделение позволяет локализовать правила валидации внутри конкретных модулей и избегать глобального загрязнения пространства проекта.


Использование кастомных валидаторов

class-validator позволяет создавать кастомные правила, которые выносятся в отдельный слой.

Пример кастомного валидатора:

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

@ValidatorConstraint({ name: 'isUsernameUnique', async: true })
export class IsUsernameUnique implements ValidatorConstraintInterface {
  async validate(username: string) {
    const user = await fakeDatabase.findUser(username);
    return !user;
  }

  defaultMessage(args: ValidationArguments) {
    return `Username ${args.value} already exists`;
  }
}

Организационно такие валидаторы следует хранить отдельно:

shared/
  validators/
    is-username-unique.validator.ts
    is-email-unique.validator.ts

Ключевая цель — повторное использование и независимость от конкретного DTO.


Регистрация и применение валидаторов

Кастомные валидаторы применяются через декораторы:

import { Validate } from 'class-validator';
import { IsUsernameUnique } from '../. ./shared/validators/is-username-unique.validator';

export class RegisterUserDto {
  @Validate(IsUsernameUnique)
  username: string;
}

Важно учитывать, что валидаторы должны быть зарегистрированы в DI-контейнере, если используется NestJS или аналогичная система.


Организация правил валидации

При росте проекта появляется проблема дублирования правил. Например, email может использоваться в нескольких DTO. Решение — вынос повторяющихся правил в переиспользуемые классы или композицию декораторов.

Пример композиции:

import { applyDecorators } from '@nestjs/common';
import { IsEmail, IsNotEmpty } from 'class-validator';

export function IsValidEmail() {
  return applyDecorators(IsEmail(), IsNotEmpty());
}

Такой подход упрощает поддержку и снижает вероятность рассинхронизации правил.


Группы валидации и сценарии использования

class-validator поддерживает группы, позволяющие разделять сценарии применения одного DTO.

export class UpdateUserDto {
  @IsString({ groups: ['update'] })
  username?: string;

  @IsString({ groups: ['create'] })
  password?: string;
}

Это позволяет использовать один класс в разных контекстах, но требует строгой дисциплины в архитектуре, чтобы не превращать DTO в перегруженные конструкции.


Связь с трансформацией данных

Валидация часто используется вместе с class-transformer, что влияет на структуру проекта.

Типичный поток данных:

  1. Получение сырого payload
  2. Трансформация в DTO
  3. Валидация через class-validator
  4. Передача в сервис

Рекомендуется выделять слой трансформации отдельно, чтобы избежать смешивания ответственности:

shared/
  pipes/
    validation.pipe.ts
    transform.pipe.ts

Пайпы как точка интеграции валидации

В архитектуре, особенно при использовании фреймворков, пайпы становятся точкой входа для валидации.

import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  transform: true,
}));

Важные параметры:

  • whitelist — удаление лишних полей
  • transform — автоматическое преобразование типов
  • forbidNonWhitelisted — строгий контроль входных данных

Пайпы должны находиться на границе системы, а не внутри бизнес-логики.


Организация повторного использования правил

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

  • базовые классы DTO
  • миксины
  • композиция декораторов
  • shared-валидаторы

Пример базового DTO:

export class PaginationDto {
  @IsInt()
  page: number;

  @IsInt()
  lim it: number;
}

И расширение:

export class GetUsersDto extends PaginationDto {
  @IsOptional()
  @IsString()
  search?: string;
}

Разделение синхронной и асинхронной валидации

Асинхронные валидаторы требуют особой организации, так как могут зависеть от базы данных или внешних сервисов. Их рекомендуется изолировать от синхронных проверок.

Практика разделения:

  • синхронные проверки — встроенные декораторы
  • асинхронные — кастомные валидаторы в отдельном слое

Это позволяет контролировать производительность и избегать блокирующих операций в критических местах.


Централизация ошибок валидации

Ошибки, возвращаемые class-validator, должны приводиться к единому формату на уровне инфраструктуры.

Типичный подход:

  • перехват ошибок в фильтре исключений
  • преобразование структуры ошибок в стандарт API response

Это требует отдельного слоя:

common/
  filters/
    validation-exception.filter.ts

Такой слой не должен зависеть от DTO или конкретных модулей.


Масштабирование структуры в крупных проектах

В больших системах важно избегать глобальных validators и неконтролируемых зависимостей. Рекомендуется модульная структура:

  • каждый модуль содержит свои DTO и валидаторы
  • shared используется только для универсальных правил
  • бизнес-валидация остаётся в сервисах

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