Асинхронная валидация используется в ситуациях, когда проверка
значения требует обращения к внешним источникам: базе данных, HTTP API,
кэшу, файловой системе или любым операциям, результат которых недоступен
синхронно. В экосистеме Class-validator поддержка асинхронных
валидаторов встроена на уровне архитектуры и позволяет возвращать
Promise<boolean> или Promise<void>
внутри пользовательских правил.
Асинхронные проверки особенно важны при работе с уникальностью данных, состоянием внешних сервисов и бизнес-логикой, завязанной на удалённые ресурсы.
В основе асинхронной валидации лежит возможность функции
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Оптимизационные подходы:
Валидаторы могут принимать параметры через
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);
}
Асинхронные валидаторы часто интегрируются с 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;
}
Особенности:
Ошибки в Promise автоматически считаются провалом
валидации, если они не обработаны.
async validate(value: string): Promise<boolean> {
try {
const result = await externalCheck(value);
return result.ok;
} catch {
return false;
}
}
Такой подход предотвращает падение процесса валидации при сбое внешнего сервиса.
Несмотря на гибкость, существуют технические ограничения:
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-моделей асинхронная валидация становится частью
пайплайна обработки запроса. Каждое поле может инициировать собственные
независимые Promise, которые объединяются в общий
результат.
class CreateOrderDto {
@IsUUID()
@ProductAvailable()
productId: string;
@IsInt()
@UserHasBalance()
userId: number;
}
Каждое правило выполняется асинхронно и участвует в итоговой агрегации ошибок.