При использовании class-validator ключевым фактором
становится не столько набор декораторов, сколько архитектура проекта, в
которой валидация занимает чётко определённое место. Ошибкой является
смешивание бизнес-логики, описания данных и правил проверки в одном
слое. Это приводит к дублированию правил, усложнению тестирования и
невозможности масштабирования.
Рациональная структура проекта предполагает разделение на несколько уровней: транспортный слой (HTTP/CLI/GraphQL), слой DTO (Data Transfer Objects), слой доменной модели и слой бизнес-логики. Валидация в таком подходе концентрируется преимущественно в DTO и частично на границах домена.
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 не должен содержать бизнес-логики. Его задача — описать форму данных и ограничения на уровне структуры.
Чёткое разделение слоёв позволяет избежать смешивания задач:
Валидация через 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,
что влияет на структуру проекта.
Типичный поток данных:
Рекомендуется выделять слой трансформации отдельно, чтобы избежать смешивания ответственности:
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:
export class PaginationDto {
@IsInt()
page: number;
@IsInt()
lim it: number;
}
И расширение:
export class GetUsersDto extends PaginationDto {
@IsOptional()
@IsString()
search?: string;
}
Асинхронные валидаторы требуют особой организации, так как могут зависеть от базы данных или внешних сервисов. Их рекомендуется изолировать от синхронных проверок.
Практика разделения:
Это позволяет контролировать производительность и избегать блокирующих операций в критических местах.
Ошибки, возвращаемые class-validator, должны приводиться
к единому формату на уровне инфраструктуры.
Типичный подход:
Это требует отдельного слоя:
common/
filters/
validation-exception.filter.ts
Такой слой не должен зависеть от DTO или конкретных модулей.
В больших системах важно избегать глобальных validators
и неконтролируемых зависимостей. Рекомендуется модульная структура:
Это позволяет сохранять предсказуемость поведения системы при росте количества модулей и команд разработки.