В Joi асинхронная валидация с обращением к внешним источникам
реализуется через механизм external-правил и validateAsync,
позволяющий подключать проверки базы данных, HTTP-сервисов и любых
других асинхронных источников данных после основной синхронной проверки
схемы.
Валидация в Joi проходит несколько этапов. Сначала выполняются
синхронные проверки: типы, обязательность, ограничения
(min, max, pattern). Только после
этого могут запускаться асинхронные шаги, связанные с внешними
источниками.
Ключевая идея заключается в разделении ответственности:
Такая модель позволяет избежать блокировки сложных проверок внутри базовой логики схемы.
Основной способ запуска асинхронной валидации — метод
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().
Он позволяет добавить функцию, которая выполняется после основной
проверки схемы.
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.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 операцию.
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;
});
Подобные проверки требуют осторожного обращения с таймаутами и отказоустойчивостью, так как внешний сервис может быть недоступен.
Ошибки, возникающие в 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-правила последовательно после основной валидации, что может создавать узкие места при большом количестве запросов.
Типичные приёмы оптимизации:
Пример кеширования:
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-валидация требует аккуратного проектирования, так как легко приводит к:
Частые ошибки:
.external() без
кеширования;External-механизм эффективно работает только при чётком разделении: Joi отвечает за валидацию данных, внешние сервисы — за проверку состояния системы, а бизнес-логика координирует эти слои.