Пользовательские паттерны

Валидация данных в прикладных системах редко ограничивается примитивными проверками строк или чисел. По мере роста доменной сложности возникает необходимость в стандартизированных правилах, которые можно повторно использовать в разных частях приложения. В контексте библиотеки Joi такие правила формируются через пользовательские паттерны — наборы схем, кастомных валидаторов и расширений, объединённых в единые строительные блоки.

Пользовательские паттерны позволяют описывать доменную логику валидации декларативно, не размазывая её по коду сервисов и контроллеров. Это особенно важно при масштабировании приложений, где одни и те же правила применяются к API, формам и внутренним структурам данных.


Базовая концепция паттернизации схем

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

Типовой паттерн строится вокруг:

  • базового типа (string, number, object)
  • набора ограничений
  • доменных правил
  • переиспользуемой композиции

Пример базового паттерна для 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() для структурных паттернов

Метод .pattern() применяется для динамических ключей объектов. Это особенно полезно при работе с конфигурациями, словарями и метаданными.

const configPattern = Joi.object()
  .pattern(
    Joi.string().min(3),
    Joi.alternatives().try(Joi.string(), Joi.number(), Joi.boolean())
  );

Здесь задаётся правило: любой ключ длиной от 3 символов может содержать значение определённых типов.


Кастомные валидаторы через .custom()

Когда встроенных механизмов недостаточно, применяется .custom(). Он позволяет внедрять произвольную логику.

const evenNumberPattern = Joi.number().custom((value, helpers) => {
  if (value % 2 !== 0) {
    return helpers.error("number.even");
  }
  return value;
}).messages({
  "number.even": "Число должно быть чётным"
});

Кастомные функции часто используются для:

  • проверки сложных бизнес-ограничений
  • обращения к внешним справочникам
  • реализации зависимых правил

Создание расширений через Joi.extend

Для систем с большим количеством доменных правил используется механизм расширений. Он позволяет создавать новые типы на основе существующих примитивов.

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)

В сложных структурах данных один и тот же параметр может иметь разные формы. Для этого используется alternatives().

const identifierPattern = Joi.alternatives().try(
  Joi.string().email(),
  Joi.string().pattern(/^@\w+$/),
  Joi.number().integer()
);

Такой паттерн полезен при обработке гибких API, где формат входных данных зависит от контекста.


Условные паттерны через when()

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

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": "Допустимо не более двух знаков после запятой"
});

При масштабировании проекта такие сообщения часто выносятся в отдельные модули.


Паттерны как слой архитектуры

При правильной организации кодовой базы паттерны становятся отдельным архитектурным слоем:

  • слой доменных правил
  • слой схем API
  • слой инфраструктурной валидации

Joi в этом контексте выступает не просто инструментом проверки, а языком описания ограничений предметной области.

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