Библиотека Joi предоставляет механизм расширений, позволяющий добавлять собственные правила валидации, типы и поведение схем без изменения исходного кода ядра. Этот механизм становится критически важным при построении крупных систем валидации, где стандартных примитивов недостаточно, а логика проверки должна быть переиспользуемой и распространяемой как отдельный модуль.
Расширение в Joi представляет собой декларативное описание набора изменений к существующим типам или добавление новых типов схем. Внутри расширение обычно состоит из нескольких ключевых частей:
Внутренне Joi применяет расширения на этапе компиляции схемы, создавая производную структуру, которая затем используется при валидации. Это означает, что расширение не является «динамическим плагином» в классическом смысле, а участвует в формировании итогового валидатора.
Создание расширения начинается с определения объекта, который описывает изменения:
import JoiBase from 'joi';
const Joi = JoiBase.extend((joi) => ({
type: 'positiveInt',
base: joi.number(),
messages: {
'positiveInt.base': '{{#label}} должно быть положительным целым числом',
},
rules: {
positive: {
validate(value, helpers) {
if (value <= 0) {
return helpers.error('positiveInt.base');
}
return value;
}
}
}
}));
Здесь происходит сразу несколько операций:
positiveInt, основанный на
number;positive;validate, которая участвует в
процессе проверки.Расширения редко существуют в виде одиночных правил. В реальных системах они объединяются в набор логически связанных проверок. Важный аспект — разделение ответственности: одно расширение должно инкапсулировать одну предметную область.
Пример группировки:
Такой подход позволяет создавать предсказуемую структуру API:
Joi.productCode().region('EU').strict()
Каждый вызов — это отдельное правило внутри расширения.
Расширения в Joi часто включают собственные сообщения ошибок. Это необходимо для сохранения согласованности с доменной моделью приложения.
Сообщения описываются через шаблоны:
messages: {
'productCode.invalid': 'Код продукта {{#value}} не соответствует формату',
'productCode.region': 'Регион {{#region}} не поддерживается'
}
Ключевым моментом является использование плейсхолдеров, которые подставляются в момент формирования ошибки. Это позволяет отделить логику валидации от текстового представления.
При упаковке расширений для многосервисных систем часто предусматривается возможность внешней подмены сообщений, что позволяет интегрировать i18n без модификации логики.
При проектировании расширений важно соблюдать принцип изоляции. Расширение не должно зависеть от глобального состояния Joi, кроме базового экземпляра.
Правильная структура модуля:
/src
index.js
rules/
messages/
types/
Файл экспорта:
export default function productCodeExtension(joi) {
return joi.extend((joi) => ({
type: 'productCode',
base: joi.string(),
rules: {
format: {
validate(value, helpers) {
if (!/^PRD-[0-9]{6}$/.test(value)) {
return helpers.error('productCode.invalid');
}
return value;
}
}
}
}));
}
Такая форма позволяет подключать расширение как функцию, не загрязняя глобальное пространство.
Расширения Joi часто распространяются как отдельные пакеты. При этом важно учитывать несколько факторов:
Joi должен быть объявлен как peer dependency:
{
"peerDependencies": {
"joi": ">=17.0.0"
}
}
Это предотвращает дублирование экземпляров библиотеки, что критично для корректной работы схем.
Поддерживаются оба формата:
module.exports = productCodeExtension;
или
export default productCodeExtension;
Минимальная структура:
package.json
index.js
README.md
src/
README часто содержит не только описание, но и примеры интеграции, поскольку расширения используются как API.
Joi активно развивает внутреннюю архитектуру, поэтому расширения должны быть чувствительны к версии ядра.
Основные риски:
extend;Для контроля совместимости используют:
При распространении расширений часто применяется транспиляция:
Особое внимание уделяется tree-shaking: расширение должно быть максимально модульным, чтобы не тянуть лишний код.
В TypeScript расширения Joi могут быть описаны через декларации модулей. Это позволяет расширять типы схем:
declare module 'joi' {
interface StringSchema {
productCode(): this;
}
}
Типизация обеспечивает:
В крупных проектах несколько расширений могут комбинироваться:
const JoiExtended = Joi
.extend(productCodeExtension)
.extend(regionExtension)
.extend(currencyExtension);
Композиция требует осторожности: порядок подключения может влиять на поведение правил, особенно если они модифицируют одно и то же значение.
Расширения Joi часто становятся отражением предметной области. Вместо простых проверок формируется слой доменных типов:
Это позволяет перенести часть бизнес-логики из сервисов в слой валидации, снижая дублирование и повышая согласованность данных.
Расширения требуют отдельного тестового покрытия:
Типичный подход — использование таблиц тест-кейсов, где каждая строка описывает вход и ожидаемый результат, что упрощает масштабирование тестов при росте расширения.