Асинхронная валидация

Асинхронная валидация в JavaScript-приложениях возникает там, где проверка данных не может быть выполнена локально и требует обращения к внешним источникам: базе данных, API, сторонним сервисам. В отличие от синхронных проверок, такие операции связаны с задержками и должны строиться на основе Promise, async/await или callback-подходов.

Библиотека validator.js предоставляет богатый набор синхронных валидаторов для строковых данных (email, URL, длина, числовые проверки и т.д.), однако не включает встроенный механизм асинхронных проверок. Это формирует архитектурный паттерн: синхронная валидация выполняется внутри Validator.js, а асинхронная логика реализуется обёртками вокруг него.


Разделение ответственности: синхронная и асинхронная валидация

Синхронные валидаторы работают мгновенно и не зависят от внешних систем:

import validator from 'validator';

validator.isEmail('test@example.com'); // true
validator.isLength('hello', { min: 2, max: 10 }); // true

Асинхронные проверки появляются, когда требуется:

  • проверка уникальности email в базе данных
  • проверка существования username через API
  • подтверждение валидности токена
  • сверка данных с внешними сервисами

Базовая модель асинхронной валидации

Асинхронная валидация строится вокруг Promise:

async function validateEmailUniqueness(email) {
  const response = await fetch(`/api/users/check-email?email=${email}`);
  const result = await response.json();

  return result.isAvailable;
}

Комбинирование с Validator.js происходит до или после асинхронной стадии:

import validator from 'validator';

async function validateUserEmail(email) {
  if (!validator.isEmail(email)) {
    return { valid: false, error: 'Некорректный формат email' };
  }

  const isUnique = await validateEmailUniqueness(email);

  if (!isUnique) {
    return { valid: false, error: 'Email уже используется' };
  }

  return { valid: true };
}

Паттерн «синхронный фильтр + асинхронная проверка»

Типовая архитектура включает два слоя:

  1. Быстрые проверки (Validator.js)
  2. Медленные проверки (API / БД)
function basicValidation(data) {
  return validator.isLength(data.username, { min: 3, max: 20 });
}

async function asyncValidation(data) {
  const res = await fetch(`/api/check-username?u=${data.username}`);
  return res.ok;
}

async function validate(data) {
  if (!basicValidation(data)) return false;
  return await asyncValidation(data);
}

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


Обработка гонок запросов (race conditions)

Асинхронная валидация в UI часто сталкивается с проблемой устаревших запросов. Например, пользователь вводит имя, и отправляется несколько запросов подряд.

let lastRequestId = 0;

async function validateUsername(username) {
  const requestId = ++lastRequestId;

  const response = await fetch(`/api/check?username=${username}`);
  const result = await response.json();

  if (requestId !== lastRequestId) {
    return null; // игнор устаревшего результата
  }

  return result.available;
}

Validator.js здесь используется только для предварительного отсечения некорректных значений:

if (!validator.isAlphanumeric(username)) {
  return false;
}

Дебаунсинг асинхронной валидации

Для предотвращения избыточных запросов применяется debounce:

function debounce(fn, delay) {
  let timeout;

  return function (...args) {
    clearTimeout(timeout);

    timeout = setTimeout(() => fn.apply(this, args), delay);
  };
}

Применение:

const validateUsernameDebounced = debounce(async (username) => {
  if (!validator.isLength(username, { min: 3 })) return;

  const res = await fetch(`/api/check?u=${username}`);
  const data = await res.json();

  console.log(data.available);
}, 400);

Интеграция с формами

Асинхронная валидация часто используется в формах регистрации.

async function validateForm(form) {
  const errors = {};

  if (!validator.isEmail(form.email)) {
    errors.email = 'Email некорректен';
  } else {
    const res = await fetch(`/api/email?e=${form.email}`);
    const data = await res.json();

    if (!data.free) {
      errors.email = 'Email занят';
    }
  }

  if (!validator.isLength(form.password, { min: 8 })) {
    errors.password = 'Пароль слишком короткий';
  }

  return errors;
}

Здесь Validator.js выполняет роль первого фильтра, уменьшая количество сетевых вызовов.


Асинхронная композиция валидаторов

Расширяемая модель строится через композицию функций:

function composeAsyncValidators(validators) {
  return async (value) => {
    for (const validatorFn of validators) {
      const result = await validatorFn(value);
      if (result !== true) return result;
    }
    return true;
  };
}

Пример использования:

const usernameValidator = composeAsyncValidators([
  (v) => validator.isAlphanumeric(v) || 'Только буквы и цифры',
  async (v) => {
    const res = await fetch(`/api/check?v=${v}`);
    const data = await res.json();
    return data.available || 'Имя занято';
  }
]);

Кэширование результатов асинхронной валидации

Для уменьшения нагрузки применяется кэш:

const cache = new Map();

async function cachedCheckEmail(email) {
  if (cache.has(email)) {
    return cache.get(email);
  }

  const res = await fetch(`/api/email?e=${email}`);
  const data = await res.json();

  cache.set(email, data.isFree);

  return data.isFree;
}

Перед кэшированием часто выполняется синтаксическая проверка через Validator.js:

if (!validator.isEmail(email)) return false;

Ошибки и обработка отказов сети

Асинхронная валидация должна учитывать нестабильность сети:

async function safeValidate(email) {
  if (!validator.isEmail(email)) {
    return { valid: false, reason: 'format' };
  }

  try {
    const res = await fetch(`/api/check?email=${email}`);
    const data = await res.json();

    return { valid: data.ok };
  } catch (e) {
    return { valid: false, reason: 'network' };
  }
}

Параллельная валидация нескольких полей

При сложных формах проверки выполняются параллельно:

async function validateAll(data) {
  const [emailRes, usernameRes] = await Promise.all([
    validateEmailUniqueness(data.email),
    fetch(`/api/check-username?u=${data.username}`).then(r => r.json())
  ]);

  return {
    email: emailRes,
    username: usernameRes.available
  };
}

Validator.js используется до запуска Promise.all для быстрой фильтрации:

if (!validator.isEmail(data.email)) {
  return { email: false };
}