Во многих прикладных системах валидация данных не ограничивается
локальными правилами. Проверка уникальности, существования сущностей,
соответствия внешним реестрам и бизнес-ограничениям требует обращения к
удалённым сервисам. Библиотека class-validator поддерживает
асинхронные валидаторы, что позволяет интегрировать HTTP-запросы и любые
внешние API прямо в слой DTO-валидации.
Ключевая особенность асинхронной модели заключается в том, что
валидаторы могут возвращать Promise<boolean>, а весь
процесс валидации становится неблокирующим и управляемым через
await на уровне validate().
Внешний API используется внутри кастомного валидатора. Логика
выносится в класс, помеченный декоратором
@ValidatorConstraint, а сама проверка выполняется в методе
validate.
Типовая структура:
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments
} from 'class-validator';
@ValidatorConstraint({ async: true })
class IsEmailAvailableConstraint {
async validate(email, args) {
const response = await fetch(`https://api.example.com/users/check-email?email=${email}`);
const data = await response.json();
return data.available === true;
}
defaultMessage(args) {
return 'Email уже используется';
}
}
Декларация асинхронности через { async: true } является
обязательной при использовании внешних запросов. Без этого результата
Promise может интерпретироваться некорректно.
После определения constraint создаётся декоратор:
import { Validate } from 'class-validator';
export function IsEmailAvailable(validationOptions) {
return function (object, propertyName) {
Validate(IsEmailAvailableConstraint, validationOptions)(
object,
propertyName
);
};
}
Использование в DTO:
class CreateUserDto {
@IsEmailAvailable()
email;
}
Вместо fetch часто применяется axios или
специализированные HTTP-клиенты, особенно при необходимости обработки
таймаутов, повторов и interceptors.
import axios from 'axios';
@ValidatorConstraint({ async: true })
class IsUsernameUniqueConstraint {
async validate(username) {
const { data } = await axios.get(
`https://api.example.com/users/exists`,
{
params: { username },
timeout: 2000
}
);
return data.exists === false;
}
}
Ключевым моментом является контроль задержек, поскольку валидация становится частью критического пути обработки запроса.
Валидация через внешние API создаёт зависимость от сетевой доступности. При проектировании логики учитываются сценарии деградации:
async validate(value) {
try {
const response = await axios.get('https://api.example.com/check', {
timeout: 1500
});
return response.data.valid === true;
} catch (error) {
return false;
}
}
Возврат false в случае ошибки API часто используется как
стратегия fail-safe, однако в некоторых системах применяется fail-open
логика, где внешняя ошибка не блокирует основную операцию.
Для уменьшения нагрузки на API применяется кэширование. Валидация часто выполняется многократно для одинаковых значений, поэтому мемоизация существенно снижает латентность.
const cache = new Map();
@ValidatorConstraint({ async: true })
class IsProductValidConstraint {
async validate(productId) {
if (cache.has(productId)) {
return cache.get(productId);
}
const { data } = await axios.get(
`https://api.example.com/products/${productId}`
);
const isValid = data.active === true;
cache.set(productId, isValid);
return isValid;
}
}
При промышленной эксплуатации вместо Map используется
Redis или аналогичное распределённое хранилище.
Некоторые сценарии требуют проверки нескольких внешних источников
одновременно. В этом случае используется Promise.all, что
снижает общее время ожидания.
async validate(value) {
const [serviceA, serviceB] = await Promise.all([
axios.get(`https://api-a.com/check/${value}`),
axios.get(`https://api-b.com/check/${value}`)
]);
return serviceA.data.ok && serviceB.data.ok;
}
Такая модель применяется при кросс-валидации данных между системами.
При массовой валидации полей возможно превышение лимитов внешнего API. Для предотвращения используются:
Пример простого throttle-слоя:
const lastCall = new Map();
function canCall(key, delay = 1000) {
const now = Date.now();
const prev = lastCall.get(key) || 0;
if (now - prev < delay) return false;
lastCall.set(key, now);
return true;
}
Использование внутри валидатора:
async validate(value) {
if (!canCall(value)) return true;
const { data } = await axios.get(`/api/check/${value}`);
return data.ok;
}
Внешние API часто представляют собой:
Каждый тип данных требует отдельной стратегии интерпретации ответа.
Пример проверки существования пользователя в реестре:
@ValidatorConstraint({ async: true })
class UserExistsConstraint {
async validate(userId) {
const { data } = await axios.get(
`https://registry.example.com/users/${userId}`
);
return data.found === true;
}
}
В сложных системах одна сущность может валидироваться по нескольким независимым правилам:
class OrderDto {
@IsProductValid()
productId;
@IsUserActive()
userId;
@IsShippingAvailable()
addressId;
}
Каждый декоратор может обращаться к отдельному API, формируя распределённую систему валидации.
class-validator выполняет проверки параллельно, однако
бизнес-логика иногда требует последовательности. Для этого используется
объединённый валидатор:
@ValidatorConstraint({ async: true })
class ComplexConstraint {
async validate(value) {
const step1 = await axios.get(`/api/step1/${value}`);
if (!step1.data.ok) return false;
const step2 = await axios.get(`/api/step2/${value}`);
return step2.data.allowed === true;
}
}
Последовательная модель применяется при зависимых проверках, где следующий запрос зависит от результата предыдущего.
Различные сервисы возвращают разные структуры данных, что требует нормализации:
function normalizeResponse(response) {
return {
valid: response.data?.result?.status === 'ok'
};
}
async validate(value) {
const response = await axios.get(`/api/check/${value}`);
const normalized = normalizeResponse(response);
return normalized.valid;
}
Выделение нормализации в отдельную функцию снижает связанность валидатора с конкретным API.
На практике выделяются несколько типов проблем:
Обработка обычно унифицируется:
async validate(value) {
try {
const { data } = await axios.get(`/api/check/${value}`);
return Boolean(data?.valid);
} catch (e) {
return false;
}
}
При большом количестве внешних проверок применяется стратегия раннего выхода:
async validate(value) {
if (!(await checkLocalRules(value))) return false;
if (!(await checkCache(value))) return false;
if (!(await checkExternalApi(value))) return false;
return true;
}
Такой подход минимизирует количество дорогих сетевых запросов.
Асинхронная валидация через внешние API особенно важна в системах:
В этих случаях class-validator становится частью
оркестрации входных данных между независимыми сервисами, где каждое поле
DTO может инициировать отдельный запрос к внешнему домену.