Валидация данных в прикладных JavaScript/TypeScript-приложениях быстро становится централизованной задачей, поскольку одинаковые правила проверки применяются в разных слоях системы: DTO, сервисах, API-моделях, формах. Дублирование декораторов и условий приводит к рассинхронизации поведения и усложнению поддержки.
Библиотека class-validator предоставляет набор механизмов, позволяющих выносить и повторно использовать валидационную логику на уровне классов, декораторов и пользовательских правил.
Самый прямой способ переиспользования — выделение набора декораторов в отдельные композиции.
import { IsString, Length, Matches } fr om 'class-validator';
export function IsUsername() {
return function (target: Object, propertyKey: string) {
IsString()(target, propertyKey);
Length(3, 20)(target, propertyKey);
Matches(/^[a-zA-Z0-9_]+$/)(target, propertyKey);
};
}
Такой подход формирует единый контракт поля, который применяется во всех DTO:
class CreateUserDto {
@IsUsername()
username: string;
}
Повторное использование достигается через инкапсуляцию набора базовых ограничений в один декоратор.
При необходимости варьировать ограничения используется фабричный подход.
import { Length } from 'class-validator';
export function IsSlug(min = 3, max = 50) {
return function (target: Object, propertyKey: string) {
Length(min, max)(target, propertyKey);
};
}
Такая структура позволяет стандартизировать правила, сохраняя гибкость:
class PostDto {
@IsSlug(5, 100)
slug: string;
}
Повторное использование усиливается через композицию функций-декораторов.
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
export function IsTitle() {
return function (target: Object, propertyKey: string) {
IsString()(target, propertyKey);
IsNotEmpty()(target, propertyKey);
MaxLength(120)(target, propertyKey);
};
}
Композиция позволяет формировать библиотеку доменных правил, а не набор разрозненных проверок.
Классовая модель TypeScript позволяет переносить валидацию через наследование.
class BaseUserDto {
@IsString()
name: string;
}
class ExtendedUserDto extends BaseUserDto {
@IsString()
role: string;
}
При такой структуре базовые правила автоматически применяются ко всем производным моделям. Это особенно эффективно при наличии нескольких версий API.
При необходимости объединения наборов правил используется композиция через пересечение типов.
import { IntersectionType } from '@nestjs/mapped-types';
class NamePart {
@IsString()
firstName: string;
}
class ContactPart {
@IsString()
email: string;
}
class UserDto extends IntersectionType(NamePart, ContactPart) {}
Такой подход позволяет собирать DTO из независимых блоков валидации, избегая дублирования.
Наиболее мощный механизм переиспользования — создание кастомных
валидаторов через ValidatorConstraint.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
@ValidatorConstraint({ name: 'isEven', async: false })
export class IsEvenConstraint implements ValidatorConstraintInterface {
validate(value: number) {
return typeof value === 'number' && value % 2 === 0;
}
defaultMessage(args: ValidationArguments) {
return `Значение ${args.value} не является чётным числом`;
}
}
Регистрация через декоратор:
import { Validate } from 'class-validator';
export function IsEven() {
return Validate(IsEvenConstraint);
}
Дальнейшее использование становится декларативным:
class NumberDto {
@IsEven()
value: number;
}
Повторное использование усиливается, если валидатор принимает параметры через контекст.
@ValidatorConstraint({ name: 'minWords', async: false })
export class MinWordsConstraint implements ValidatorConstraintInterface {
validate(value: string, args: ValidationArguments) {
const [min] = args.constraints;
return typeof value === 'string' && value.split(' ').length >= min;
}
}
Фабрика:
export function MinWords(min: number) {
return Validate(MinWordsConstraint, [min]);
}
Повторное использование становится эффективнее при разделении уровней:
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
async validate(email: string) {
return await fakeDbCheck(email);
}
}
Инкапсуляция таких правил позволяет использовать их в разных DTO без изменения бизнес-логики.
При росте проекта набор декораторов часто выделяется в отдельный пакет:
// validation-rules package
export const Rules = {
username: IsUsername,
slug: IsSlug,
title: IsTitle,
};
Использование:
class ArticleDto {
@Rules.title()
title: string;
@Rules.slug(5, 80)
slug: string;
}
Это формирует единый источник истины для всех сервисов.
Некоторые сценарии требуют динамического переиспользования правил. В таких случаях применяется метаданные:
import 'reflect-metadata';
const VALIDATION_KEY = 'validation_rules';
export function AddRule(rule: any) {
return function (target: any, propertyKey: string) {
const existing = Reflect.getMetadata(VALIDATION_KEY, target, propertyKey) || [];
Reflect.defineMetadata(VALIDATION_KEY, [...existing, rule], target, propertyKey);
};
}
Это позволяет централизованно применять правила на основе внешней конфигурации.
Базовые модели часто становятся контейнером повторно используемых полей:
class PaginationDto {
@IsNumber()
page: number;
@IsNumber()
lim it: number;
}
Далее они включаются в другие DTO:
class ListUsersDto extends PaginationDto {
@IsOptional()
@IsString()
search?: string;
}
Группы позволяют повторно использовать одно и то же поле в разных сценариях:
class UserDto {
@IsString({ groups: ['create'] })
password: string;
@IsString({ groups: ['update'] })
id: string;
}
Использование групп снижает необходимость создавать отдельные классы под каждый сценарий.
Миксины позволяют динамически создавать классы с повторно используемой логикой:
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
@IsDate()
createdAt: Date;
@IsDate()
updatedAt: Date;
};
}
Применение:
class UserBase {
@IsString()
name: string;
}
class UserEntity extends Timestamped(UserBase) {}
При сложных доменных моделях логика валидации переносится в отдельные спецификации:
export const UserValidationSpec = {
username: [IsUsername()],
email: [IsEmail()],
};
Далее применяется программно:
class UserDto {
@ApplyRules(UserValidationSpec.username)
username: string;
}
Такой подход обеспечивает единый источник правил вне классовой структуры.