Валидация, опирающаяся на внешние источники, используется тогда, когда локального анализа данных недостаточно для принятия решения о корректности ввода. Речь идёт о сценариях, где требуется обращение к API, базе данных или стороннему сервису: проверка уникальности логина, подтверждение существования email, сверка промокода, проверка доступности имени ресурса или валидация бизнес-правил, зависящих от серверного состояния.
В контексте Vest такие проверки интегрируются в цепочку правил как асинхронные операции, при этом остаётся возможность сохранять декларативный стиль описания правил и управляемость состояния ошибок.
Vest поддерживает асинхронные проверки, что позволяет включать запросы к внешним источникам прямо в описание схемы валидации. Ключевая особенность заключается в том, что такие правила выполняются не мгновенно, а возвращают Promise, влияя на итоговое состояние формы после завершения запроса.
Типичный сценарий — проверка уникальности username:
import { create, test, enforce } from 'vest';
const suite = create((data) => {
test('username', 'Имя пользователя уже занято', async () => {
const res = await fetch(`/api/users/check?username=${data.username}`);
const json = await res.json();
enforce(json.available).isTruthy();
});
});
Асинхронность здесь не усложняет структуру тестов, но накладывает требования к управлению состоянием выполнения и оптимизации количества запросов.
Прямое выполнение запросов в каждом изменении поля приводит к избыточной нагрузке на сервер. При работе с внешними источниками важно учитывать несколько факторов:
Vest не навязывает конкретную стратегию оптимизации, поэтому управление этими аспектами реализуется на уровне пользовательской логики.
Дебаунсинг позволяет ограничить количество обращений к внешнему источнику при частых изменениях значения поля.
let timeout;
const checkUsername = (username) =>
new Promise((resolve) => {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const res = await fetch(`/api/users/check?username=${username}`);
const json = await res.json();
resolve(json.available);
}, 400);
});
Использование такой функции внутри Vest-правила снижает нагрузку на сервер и повышает стабильность интерфейса.
При асинхронной валидации часто возникает ситуация, когда более ранний запрос завершается позже нового, перезаписывая актуальный результат. Это приводит к неконсистентному состоянию ошибки.
Решение заключается в использовании идентификаторов запросов или
AbortController.
let controller;
const checkEmail = async (email) => {
if (controller) controller.abort();
controller = new AbortController();
const res = await fetch(`/api/email/check?email=${email}`, {
signal: controller.signal,
});
const json = await res.json();
return json.valid;
};
Интеграция с Vest:
test('email', 'Email уже используется', async () => {
const valid = await checkEmail(data.email);
enforce(valid).isTruthy();
});
Такой подход гарантирует, что только последний запрос влияет на состояние валидации.
Повторяющиеся запросы к внешнему источнику могут быть устранены через кэширование. Это особенно важно при проверке уникальности значений, где вероятность повторов высока.
Простейшая реализация:
const cache = new Map();
const checkUsernameCached = async (username) => {
if (cache.has(username)) {
return cache.get(username);
}
const res = await fetch(`/api/users/check?username=${username}`);
const json = await res.json();
cache.set(username, json.available);
return json.available;
};
В контексте Vest это снижает задержки валидации и уменьшает количество сетевых вызовов без изменения структуры тестов.
Валидация с внешними источниками редко используется изолированно. Обычно она комбинируется с локальными правилами: проверкой формата, длины и синтаксиса.
test('username', 'Обязательное поле', () => {
enforce(data.username).isNotEmpty();
});
test('username', 'Недопустимый формат', () => {
enforce(data.username).matches(/^[a-zA-Z0-9_]+$/);
});
test('username', 'Имя уже занято', async () => {
if (data.username.length < 3) return;
const available = await checkUsernameCached(data.username);
enforce(available).isTruthy();
});
Локальные проверки выполняются мгновенно, а внешние — только при прохождении базовых условий. Это снижает количество ненужных запросов и ускоряет отклик интерфейса.
Асинхронные тесты могут выполняться параллельно, но иногда требуется контроль последовательности. Например, нет смысла проверять доступность имени, если оно уже не проходит локальную валидацию.
Vest позволяет использовать условные конструкции внутри suite:
test('username', 'Слишком короткое имя', () => {
enforce(data.username.length).greaterThan(2);
});
if (data.username.length > 2) {
test('username', 'Недоступное имя', async () => {
const available = await checkUsernameCached(data.username);
enforce(available).isTruthy();
});
}
Такой подход уменьшает количество асинхронных операций и упрощает анализ ошибок.
Внешние источники могут возвращать ошибки сети, таймауты или некорректные ответы. Игнорирование этих случаев приводит к нестабильной валидации.
test('email', 'Ошибка проверки email', async () => {
try {
const valid = await checkEmail(data.email);
enforce(valid).isTruthy();
} catch (e) {
enforce(false).equals(true);
}
});
Более гибкий подход — трактовать недоступность сервиса как временную неопределённость, а не как ошибку формы:
test('email', 'Не удалось проверить email', async () => {
try {
const valid = await checkEmail(data.email);
enforce(valid).isTruthy();
} catch {
return;
}
});
Некоторые проверки невозможно воспроизвести на клиенте: ограничения тарифов, доступность функций, региональные правила. В таких случаях сервер возвращает структурированные ошибки, которые интерпретируются как часть валидационного процесса.
const validateBusinessRules = async (data) => {
const res = await fetch('/api/validate', {
method: 'POST',
body: JSON.stringify(data),
});
if (!res.ok) {
const errors = await res.json();
return errors;
}
return null;
};
Использование в Vest:
test('form', 'Ошибка бизнес-валидации', async () => {
const errors = await validateBusinessRules(data);
if (errors) {
enforce(false).equals(true);
}
});
Асинхронная проверка всегда связана с задержкой, поэтому важно минимизировать ощущение ожидания. Это достигается комбинацией нескольких техник:
Такой набор позволяет сохранить отзывчивость интерфейса даже при сложной серверной логике.
При увеличении количества внешних проверок важно сохранять читаемость. В Vest это достигается разбиением логики на модули:
const checkers = {
username: async (value) => checkUsernameCached(value),
email: async (value) => checkEmail(value),
};
И использование их в suite:
test('username', 'Имя занято', async () => {
const available = await checkers.username(data.username);
enforce(available).isTruthy();
});
Такой подход отделяет бизнес-логику от описания валидации и упрощает тестирование отдельных компонентов.
Важный аспект — реакция интерфейса на незавершённые асинхронные проверки. Часто используется промежуточное состояние, при котором поле считается “в процессе проверки”. Vest позволяет отслеживать выполнение suite и строить на этом основе UI-индикацию загрузки.
Асинхронная природа внешней валидации требует учитывать, что итоговое состояние ошибки может измениться после завершения запроса, поэтому состояние формы должно быть реактивным к завершению всех тестов, а не к моменту их запуска.