Валидация с внешними источниками

Природа внешней валидации и её место в логике формы

Валидация, опирающаяся на внешние источники, используется тогда, когда локального анализа данных недостаточно для принятия решения о корректности ввода. Речь идёт о сценариях, где требуется обращение к API, базе данных или стороннему сервису: проверка уникальности логина, подтверждение существования email, сверка промокода, проверка доступности имени ресурса или валидация бизнес-правил, зависящих от серверного состояния.

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

Асинхронные правила и их особенности в 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();
  });
});

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

Управление сетевыми запросами внутри валидации

Прямое выполнение запросов в каждом изменении поля приводит к избыточной нагрузке на сервер. При работе с внешними источниками важно учитывать несколько факторов:

  • частота ввода пользователем данных;
  • задержки сети;
  • повторяющиеся запросы с одинаковыми значениями;
  • конкуренция ответов (race conditions).

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-индикацию загрузки.

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