Валидация данных в прикладных системах редко ограничивается примитивными проверками строк или чисел. По мере роста доменной сложности возникает необходимость в стандартизированных правилах, которые можно повторно использовать в разных частях приложения. В контексте библиотеки Joi такие правила формируются через пользовательские паттерны — наборы схем, кастомных валидаторов и расширений, объединённых в единые строительные блоки.
Пользовательские паттерны позволяют описывать доменную логику валидации декларативно, не размазывая её по коду сервисов и контроллеров. Это особенно важно при масштабировании приложений, где одни и те же правила применяются к API, формам и внутренним структурам данных.
Joi предоставляет возможность собирать сложные схемы из простых элементов. Основная идея заключается в том, что схема становится не просто набором проверок, а переиспользуемым модулем.
Типовой паттерн строится вокруг:
Пример базового паттерна для email:
const emailPattern = Joi.string()
.email({ tlds: { allow: false } })
.lowercase()
.trim()
.required();
Такой объект может использоваться во множестве схем без дублирования логики.
В реальных приложениях паттерны часто отражают бизнес-логику. Например, идентификаторы пользователей, номера телефонов или артикулы товаров.
Паттерн телефонного номера:
const phonePattern = Joi.string()
.pattern(/^\+?[1-9]\d{7,14}$/)
.messages({
"string.pattern.base": "Некорректный формат телефона"
});
Важно, что паттерн не привязан к конкретному месту использования. Он становится атомарным правилом, которое можно включать в любые структуры.
Joi позволяет строить сложные объекты, комбинируя ранее созданные паттерны:
const userPattern = Joi.object({
id: Joi.number().integer().positive(),
email: emailPattern,
phone: phonePattern,
createdAt: Joi.date().iso()
});
Такой подход формирует слой переиспользуемых блоков, где каждая часть отвечает за свою зону ответственности.
Метод .pattern() применяется для динамических ключей
объектов. Это особенно полезно при работе с конфигурациями, словарями и
метаданными.
const configPattern = Joi.object()
.pattern(
Joi.string().min(3),
Joi.alternatives().try(Joi.string(), Joi.number(), Joi.boolean())
);
Здесь задаётся правило: любой ключ длиной от 3 символов может содержать значение определённых типов.
Когда встроенных механизмов недостаточно, применяется
.custom(). Он позволяет внедрять произвольную логику.
const evenNumberPattern = Joi.number().custom((value, helpers) => {
if (value % 2 !== 0) {
return helpers.error("number.even");
}
return value;
}).messages({
"number.even": "Число должно быть чётным"
});
Кастомные функции часто используются для:
Для систем с большим количеством доменных правил используется механизм расширений. Он позволяет создавать новые типы на основе существующих примитивов.
const JoiExtended = Joi.extend((joi) => ({
type: "positiveInt",
base: joi.number().integer(),
validate(value, helpers) {
if (value <= 0) {
return { value, errors: helpers.error("positiveInt.base") };
}
},
messages: {
"positiveInt.base": "Значение должно быть положительным целым числом"
}
}));
После этого новый тип становится частью системы валидации:
const schema = JoiExtended.object({
score: JoiExtended.positiveInt()
});
Расширения позволяют формировать полноценную библиотеку доменных типов поверх Joi.
В сложных структурах данных один и тот же параметр может иметь разные
формы. Для этого используется alternatives().
const identifierPattern = Joi.alternatives().try(
Joi.string().email(),
Joi.string().pattern(/^@\w+$/),
Joi.number().integer()
);
Такой паттерн полезен при обработке гибких API, где формат входных данных зависит от контекста.
Паттерны могут изменяться в зависимости от других полей объекта.
const passwordSchema = Joi.object({
password: Joi.string().min(8),
isAdmin: Joi.boolean(),
secretKey: Joi.when("isAdmin", {
is: true,
then: Joi.string().required(),
otherwise: Joi.forbidden()
})
});
Это позволяет строить адаптивные схемы, зависящие от состояния данных.
Для масштабируемых систем паттерны часто оформляются как функции-фабрики:
const createSlugPattern = () =>
Joi.string()
.lowercase()
.pattern(/^[a-z0-9-]+$/)
.min(3)
.max(50);
Фабрики полезны, когда:
Массивы часто требуют строгой структуры элементов:
const tagsPattern = Joi.array()
.items(Joi.string().lowercase().trim())
.unique()
.min(1);
Для сложных случаев:
const matrixPattern = Joi.array().items(
Joi.array().items(Joi.number())
);
В зрелых системах паттерны объединяются в более крупные конструкции:
const addressPattern = Joi.object({
country: Joi.string().required(),
city: Joi.string().required(),
zip: Joi.string().pattern(/^\d{5}$/)
});
const profilePattern = Joi.object({
name: Joi.string().min(2),
address: addressPattern,
contacts: Joi.object({
email: emailPattern,
phone: phonePattern
})
});
Так формируется иерархия правил, отражающая структуру предметной области.
Ключевой аспект пользовательских паттернов — единообразие ошибок. Joi позволяет централизованно задавать сообщения:
const currencyPattern = Joi.number().precision(2).messages({
"number.base": "Значение должно быть числом",
"number.precision": "Допустимо не более двух знаков после запятой"
});
При масштабировании проекта такие сообщения часто выносятся в отдельные модули.
При правильной организации кодовой базы паттерны становятся отдельным архитектурным слоем:
Joi в этом контексте выступает не просто инструментом проверки, а языком описания ограничений предметной области.
Использование пользовательских паттернов позволяет минимизировать дублирование логики, повысить согласованность данных и упростить сопровождение системы.