Создание класса для валидации

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

Класс для валидации представляет собой обычный TypeScript/JavaScript класс, свойства которого аннотируются декораторами из библиотеки class-validator. Каждый декоратор описывает правило, которому должно соответствовать значение поля.

Простейший пример:

import { IsString, IsInt } from 'class-validator';

class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;
}

В этом примере класс CreateUserDto не содержит логики проверки. Он лишь описывает структуру данных. Проверка происходит отдельно через функцию validate.

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

Классы валидации часто называют DTO (Data Transfer Object). Их задача — формализовать контракт входных данных между слоями приложения. Такой класс:

  • фиксирует ожидаемую структуру объекта;
  • задаёт ограничения для каждого поля;
  • позволяет централизованно изменять правила валидации;
  • предотвращает попадание некорректных данных в бизнес-логику.

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

Подключение декораторов

Для работы декораторов требуется включение поддержки в TypeScript:

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

Также необходимо установить reflect-metadata:

import 'reflect-metadata';

Без этого механизма class-validator не сможет анализировать типы свойств и корректно применять часть правил.

Создание класса с базовыми типами

На практике класс начинается с определения простых полей:

import { IsString, IsNumber, IsBoolean } from 'class-validator';

class ProductDto {
  @IsString()
  title: string;

  @IsNumber()
  price: number;

  @IsBoolean()
  inStock: boolean;
}

Каждый декоратор описывает ожидаемый тип. Если входящее значение не соответствует типу, валидация вернёт ошибку.

Обязательные и необязательные поля

По умолчанию поля считаются обязательными. Для управления этим поведением используется @IsOptional().

import { IsOptional, IsString } from 'class-validator';

class UpdateProductDto {
  @IsOptional()
  @IsString()
  title?: string;
}

Порядок декораторов важен: сначала @IsOptional(), затем остальные проверки. Это означает, что остальные валидаторы будут применяться только если значение присутствует.

Использование строковых ограничений

Для строк часто применяются дополнительные ограничения:

import { IsString, Length, Matches } from 'class-validator';

class UserDto {
  @IsString()
  @Length(3, 20)
  username: string;

  @Matches(/^[a-z0-9_]+$/)
  nickname: string;
}

@Length(min, max) задаёт допустимую длину строки. @Matches() позволяет ограничить формат с помощью регулярного выражения.

Числовые ограничения

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

import { IsNumber, Min, Max } from 'class-validator';

class PaymentDto {
  @IsNumber()
  @Min(1)
  @Max(10000)
  amount: number;
}

Такие ограничения полезны при проверке бизнес-правил на уровне входных данных.

Вложенные классы

Одним из ключевых механизмов является поддержка вложенной валидации. Для этого используется @ValidateNested() совместно с @Type() из class-transformer.

import { ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';

class AddressDto {
  @IsString()
  city: string;

  @IsString()
  street: string;
}

class UserDto {
  @IsString()
  name: string;

  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}

Без @Type() вложенный объект не будет корректно преобразован в экземпляр класса, и валидация не выполнится полностью.

Работа с массивами

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

import { IsArray, IsString } from 'class-validator';

class GroupDto {
  @IsArray()
  @IsString({ each: true })
  tags: string[];
}

Параметр each: true указывает, что правило применяется к каждому элементу массива.

Булевы и числовые преобразования

Входные данные часто приходят в виде строк. Для приведения типов используется class-transformer:

import { Type } from 'class-transformer';
import { IsBoolean } from 'class-validator';

class SettingsDto {
  @Type(() => Boolean)
  @IsBoolean()
  isActive: boolean;
}

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

Условная валидация

В сложных сценариях применяется условная проверка через @ValidateIf():

import { ValidateIf, IsString } from 'class-validator';

class SearchDto {
  @ValidateIf(o => o.type === 'advanced')
  @IsString()
  query: string;
}

Поле query проверяется только при выполнении условия.

Кастомные правила в классе

При необходимости создаются собственные декораторы:

import { registerDecorator, ValidationOptions } from 'class-validator';

function IsEven(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isEven',
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: {
        validate(value: number) {
          return typeof value === 'number' && value % 2 === 0;
        },
      },
    });
  };
}

class NumberDto {
  @IsEven()
  value: number;
}

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

Наследование классов валидации

Классы могут наследоваться, что позволяет переиспользовать правила:

class BaseUserDto {
  @IsString()
  name: string;
}

class ExtendedUserDto extends BaseUserDto {
  @IsNumber()
  age: number;
}

Все декораторы базового класса сохраняются и участвуют в валидации наследника.

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

В архитектуре часто требуется создавать вариации одного DTO. Это достигается через композицию классов и условные поля с @IsOptional(). Такой подход позволяет избежать дублирования и поддерживать единый источник правил.

Инициализация экземпляра для валидации

Перед проверкой объект должен быть преобразован в экземпляр класса:

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

const dto = plainToInstance(CreateUserDto, requestBody);
const errors = await validate(dto);

Без преобразования декораторы не будут применены корректно, поскольку проверка работает на уровне экземпляра класса.

Строгая типизация как часть валидации

Использование TypeScript усиливает систему class-validator, но не заменяет её. Типы исчезают в рантайме, поэтому именно декораторы обеспечивают проверку фактических значений.

Класс валидации становится единственным источником истины для структуры данных на этапе выполнения, обеспечивая согласованность между слоями приложения.