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

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

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


Поддержка Promise в механизме валидации

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

Типичная сигнатура:

validate(value: any): Promise<boolean> | boolean;

Если возвращается Promise, библиотека ожидает его разрешения перед формированием результата валидации.


Создание асинхронного кастомного валидатора

Основной способ расширения логики — использование декоратора ValidatorConstraint с установкой флага async: true.

Базовая структура

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

@ValidatorConstraint({ async: true })
export class IsEmailAlreadyExist implements ValidatorConstraintInterface {

  async validate(email: string, args: ValidationArguments): Promise<boolean> {
    const user = await fakeDatabaseFind(email);
    return !user;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Пользователь с таким email уже существует';
  }
}

Регистрация валидатора через декоратор

После создания класса валидатора он подключается через registerDecorator.

import { registerDecorator, ValidationOptions } from 'class-validator';
import { IsEmailAlreadyExist } from './validators/IsEmailAlreadyExist';

export function EmailNotTaken(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsEmailAlreadyExist,
    });
  };
}

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

class UserDto {
  @EmailNotTaken({ message: 'Email занят' })
  email: string;
}

Асинхронная проверка через базу данных

Частый сценарий — проверка уникальности записи.

async validate(username: string): Promise<boolean> {
  const existingUser = await this.userRepository.findOne({
    wh ere: { username }
  });

  return !existingUser;
}

Такая проверка не может быть выполнена синхронно, так как требует обращения к базе данных.


Работа с зависимостями в асинхронных валидаторах

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

import { useContainer } from 'class-validator';
import { Container } from 'typedi';

useContainer(Container);

После этого валидаторы могут получать зависимости через конструктор:

@ValidatorConstraint({ async: true })
export class IsUserExists implements ValidatorConstraintInterface {

  constructor(private userService: UserService) {}

  async validate(id: number): Promise<boolean> {
    return await this.userService.exists(id);
  }
}

Возврат ошибок в асинхронных проверках

Асинхронный валидатор не обязан выбрасывать исключения. Ошибки формируются через возвращаемое значение:

  • true — значение валидно
  • false — значение невалидно

Для динамических сообщений используется defaultMessage:

defaultMessage(args: ValidationArguments) {
  return `Запись ${args.value} не найдена`;
}

Комбинирование нескольких асинхронных валидаторов

При наличии нескольких асинхронных правил они выполняются параллельно. Это важно для производительности:

class ProductDto {

  @IsUUID()
  @ProductExists()
  @IsNotArchived()
  id: string;
}

Каждый валидатор возвращает Promise, а Class-validator агрегирует их результаты.


Влияние на производительность

Асинхронная валидация напрямую влияет на latency запросов.

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

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

Оптимизационные подходы:

  • кэширование результатов проверок
  • объединение запросов в batch-операции
  • минимизация количества внешних вызовов
  • использование индексов в базе данных для быстрых lookup-запросов

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

Валидаторы могут принимать параметры через constraints.

registerDecorator({
  target: object.constructor,
  propertyName,
  constraints: ['admin'],
  validator: RoleExistsConstraint,
});

Использование внутри валидатора:

async validate(role: string, args: ValidationArguments) {
  const [requiredType] = args.constraints;
  return await this.roleService.exists(role, requiredType);
}

Обработка внешних API

Асинхронные валидаторы часто интегрируются с HTTP-запросами:

async validate(ip: string): Promise<boolean> {
  const response = await fetch(`https://api.example.com/ip/${ip}`);
  const data = await response.json();

  return data.isValid === true;
}

Особенности:

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

Обработка ошибок внутри async validate

Ошибки в Promise автоматически считаются провалом валидации, если они не обработаны.

async validate(value: string): Promise<boolean> {
  try {
    const result = await externalCheck(value);
    return result.ok;
  } catch {
    return false;
  }
}

Такой подход предотвращает падение процесса валидации при сбое внешнего сервиса.


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

Несмотря на гибкость, существуют технические ограничения:

  • нельзя использовать синхронный результат внешнего API
  • задержка всех проверок влияет на общий response time
  • невозможна частичная отмена Promise внутри pipeline валидации
  • сложнее тестировать без мокирования зависимостей

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

Репозиторный паттерн

async validate(email: string): Promise<boolean> {
  return this.userRepository.isEmailFree(email);
}

Сервисный слой

async validate(userId: number): Promise<boolean> {
  return this.userService.canBeDeleted(userId);
}

Агрегатор проверок

async validate(value: string): Promise<boolean> {
  const [exists, allowed] = await Promise.all([
    this.checkExists(value),
    this.checkPermission(value)
  ]);

  return exists && allowed;
}

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

Для предотвращения зависания можно ограничивать время выполнения:

async validate(value: string): Promise<boolean> {
  const timeout = new Promise(resolve =>
    setTimeout(() => resolve(false), 2000)
  );

  const check = this.externalService.validate(value);

  return await Promise.race([check, timeout]) as boolean;
}

Асинхронные валидаторы в цепочках DTO

При использовании DTO-моделей асинхронная валидация становится частью пайплайна обработки запроса. Каждое поле может инициировать собственные независимые Promise, которые объединяются в общий результат.

class CreateOrderDto {

  @IsUUID()
  @ProductAvailable()
  productId: string;

  @IsInt()
  @UserHasBalance()
  userId: number;
}

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