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

В Joi асинхронная валидация с обращением к внешним источникам реализуется через механизм external-правил и validateAsync, позволяющий подключать проверки базы данных, HTTP-сервисов и любых других асинхронных источников данных после основной синхронной проверки схемы.

Валидация в Joi проходит несколько этапов. Сначала выполняются синхронные проверки: типы, обязательность, ограничения (min, max, pattern). Только после этого могут запускаться асинхронные шаги, связанные с внешними источниками.

Ключевая идея заключается в разделении ответственности:

  • синхронная часть схемы отвечает за структуру и формат данных;
  • external-валидация отвечает за бизнес-правила, зависящие от внешнего состояния.

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

Метод validateAsync и работа с Promise

Основной способ запуска асинхронной валидации — метод validateAsync, который возвращает Promise.

import Joi from 'joi';

const schema = Joi.object({
  email: Joi.string().email().required()
});

async function runValidation(data) {
  try {
    const result = await schema.validateAsync(data);
    return result;
  } catch (err) {
    throw err;
  }
}

Особенность заключается в том, что validateAsync учитывает не только встроенные асинхронные механизмы Joi, но и любые кастомные external-правила, подключённые к схеме.

Правило external: добавление асинхронных проверок

External-валидация реализуется через метод .external(). Он позволяет добавить функцию, которая выполняется после основной проверки схемы.

const schema = Joi.object({
  username: Joi.string().min(3).required()
}).external(async (value, helpers) => {
  const exists = await fakeDatabaseCheck(value.username);

  if (exists) {
    throw new Error('Пользователь уже существует');
  }

  return value;
});

Здесь важно, что:

  • функция получает уже провалидированное значение;
  • можно выполнять любые асинхронные операции;
  • ошибка может быть выброшена как Error или через helpers.error().

Контекст helpers и проброс состояния

Объект helpers предоставляет доступ к дополнительной информации о процессе валидации:

  • helpers.path — путь к текущему полю;
  • helpers.state — внутреннее состояние валидации;
  • helpers.message — кастомизация ошибок.

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

const schema = Joi.object({
  email: Joi.string().email().required()
}).external(async (value, helpers) => {
  const isBlocked = await checkBlacklist(value.email);

  if (isBlocked) {
    return helpers.error('any.custom', {
      message: 'Email находится в чёрном списке'
    });
  }

  return value;
});

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

Интеграция с базой данных

Наиболее частый сценарий external-валидации — проверка уникальности данных в базе.

const schema = Joi.object({
  username: Joi.string().min(3).required()
}).external(async (value) => {
  const user = await db.users.findOne({
    username: value.username
  });

  if (user) {
    throw new Error('Username уже занят');
  }

  return value;
});

Такая проверка не может быть выполнена синхронно, поэтому external становится единственным корректным местом её размещения.

Важно учитывать, что каждая external-функция увеличивает время валидации, так как добавляет I/O операцию.

Вызовы внешних HTTP API

External-валидация часто используется для интеграции с внешними сервисами: платёжными системами, сервисами верификации, антиспам-API.

import axios from 'axios';

const schema = Joi.object({
  ip: Joi.string().ip().required()
}).external(async (value) => {
  const response = await axios.get(
    `https://api.example.com/check-ip/${value.ip}`
  );

  if (response.data.blocked) {
    throw new Error('IP заблокирован внешним сервисом');
  }

  return value;
});

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

Обработка ошибок и формирование ValidationError

Ошибки, возникающие в external-валидации, могут быть двух типов:

  • стандартные Error;
  • ошибки, созданные через helpers.error.

Joi преобразует их в ValidationError.

const schema = Joi.object({
  email: Joi.string().email()
}).external(async (value, helpers) => {
  if (!value.email.endsWith('@company.com')) {
    return helpers.error('any.invalid', {
      message: 'Допустимы только корпоративные email'
    });
  }

  return value;
});

Разница подходов:

  • throw new Error() — простая ошибка без структурирования;
  • helpers.error() — позволяет формировать детализированную структуру ошибки.

Параллелизм и производительность

При работе с external-валидацией важно учитывать последовательность выполнения. Joi выполняет external-правила последовательно после основной валидации, что может создавать узкие места при большом количестве запросов.

Типичные приёмы оптимизации:

  • агрегация запросов (batching);
  • кеширование результатов проверок;
  • минимизация количества external-вызовов;
  • перенос части логики в синхронные проверки.

Пример кеширования:

const cache = new Map();

const schema = Joi.object({
  username: Joi.string()
}).external(async (value) => {
  if (cache.has(value.username)) {
    return value;
  }

  const exists = await dbCheck(value.username);
  cache.set(value.username, exists);

  if (exists) {
    throw new Error('Уже существует');
  }

  return value;
});

Ограничения и типичные ошибки

External-валидация требует аккуратного проектирования, так как легко приводит к:

  • дублированию логики между слоями приложения;
  • увеличению времени ответа API;
  • неконтролируемым зависимостям от внешних сервисов.

Частые ошибки:

  • выполнение тяжёлых операций внутри .external() без кеширования;
  • отсутствие обработки таймаутов HTTP-запросов;
  • использование external-валидации для логики, которую можно выразить через стандартные Joi-правила;
  • выброс необработанных исключений без структурирования ошибок.

External-механизм эффективно работает только при чётком разделении: Joi отвечает за валидацию данных, внешние сервисы — за проверку состояния системы, а бизнес-логика координирует эти слои.