Синхронные и асинхронные валидаторы

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

В основе работы библиотеки лежит идея декларативного описания правил через декораторы и последующего запуска механизма проверки, который обходит свойства объекта и применяет к ним набор валидаторов. Каждый валидатор может быть синхронным либо асинхронным, и это напрямую влияет на общий результат выполнения функции validate.


Синхронные валидаторы выполняются мгновенно и возвращают результат в виде boolean. Они не предполагают ожидания внешних ресурсов и полностью работают в пределах текущего вызова функции.

Наиболее распространённый сценарий — проверка формата, диапазона значений, длины строк, соответствия регулярным выражениям и других детерминированных условий.

Примеры встроенных синхронных валидаторов:

  • @IsString()
  • @IsNumber()
  • @Length()
  • @Matches()
  • @IsBoolean()

Пример использования:

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

class UserDto {
  @IsString()
  name;

  @IsNumber()
  age;

  @IsString()
  @Length(5, 20)
  password;
}

При выполнении валидации:

import { validate } from 'class-validator';

const user = new UserDto();
user.name = 123;
user.age = 'old';
user.password = '123';

validate(user).then(errors => {
  console.log(errors);
});

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

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


Асинхронные валидаторы

Асинхронные валидаторы используются в случаях, когда проверка требует обращения к внешним источникам: базе данных, API, файловой системе или другим сервисам. Они возвращают Promise<boolean> или Promise<void> и интегрируются в общий процесс через механизм await.

Для создания асинхронного кастомного валидатора используется ValidatorConstraint с параметром async: true.

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

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

@ValidatorConstraint({ async: true })
class IsEmailAlreadyExists implements ValidatorConstraintInterface {
  async validate(email) {
    const user = await database.findUserByEmail(email);
    return !user;
  }

  defaultMessage() {
    return 'Email already exists';
  }
}

Использование в DTO:

import { Validate } from 'class-validator';

class RegisterDto {
  @Validate(IsEmailAlreadyExists)
  email;
}

Поведение асинхронной валидации в pipeline

При вызове validate() библиотека определяет наличие асинхронных валидаторов и автоматически переключает выполнение в режим работы с Promise. В результате возвращается Promise<ValidationError[]>, даже если часть валидаторов синхронная.

const errors = await validate(dto);

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

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


Смешанный режим работы

В реальных приложениях синхронные и асинхронные валидаторы часто используются совместно. Например:

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

class ProfileDto {
  @IsString()
  username;

  @Length(6, 30)
  password;

  @Validate(IsEmailAlreadyExists)
  email;
}

В этом случае:

  • username и password проверяются синхронно
  • email проверяется асинхронно через обращение к базе данных

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


Внутренние особенности обработки async-валидаторов

Механизм class-validator строится вокруг обхода метаданных, сохранённых декораторами. Для каждого свойства формируется список правил, которые выполняются через Promise.all или аналогичный механизм объединения результатов.

Ключевые особенности:

  • синхронные валидаторы оборачиваются в Promise для унификации пайплайна
  • асинхронные валидаторы выполняются через await
  • ошибки агрегируются после завершения всех проверок
  • отсутствие одного await в верхнем уровне приводит к потере результата

Ограничения асинхронной валидации

Асинхронные валидаторы вводят дополнительные накладные расходы:

  • увеличение времени ответа из-за сетевых запросов
  • потенциальные узкие места при массовой валидации
  • зависимость от доступности внешних сервисов

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

Пример неэффективного подхода:

const users = arrayOfUsers.map(u => validate(u));
await Promise.all(users);

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


Контракт возврата значений

Синхронные валидаторы:

  • возвращают true или false
  • либо строку сообщения (в кастомных сценариях)

Асинхронные валидаторы:

  • возвращают Promise<boolean>
  • либо Promise<void> с выбросом ошибки через defaultMessage

Нарушение этого контракта приводит к непредсказуемому поведению системы валидации.


Использование зависимостей в async-валидации

Асинхронные валидаторы часто применяются для внедрения зависимостей через DI-контейнеры (например, в NestJS), что позволяет инкапсулировать доступ к данным:

@ValidatorConstraint({ async: true })
class IsUniqueUsername {
  constructor(private userService) {}

  async validate(username) {
    return !(await this.userService.exists(username));
  }
}

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


Влияние на архитектуру приложения

Разделение валидаторов на синхронные и асинхронные влияет на структуру DTO и слой обработки запросов:

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

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