Разделение схем по доменам

При построении валидации данных в реальных приложениях использование единой монолитной схемы быстро приводит к снижению поддерживаемости. Схемы начинают разрастаться, переплетаться между собой и терять читаемость. В таких условиях эффективным подходом становится разделение схем по доменам, при котором каждая предметная область приложения получает собственный набор независимых или слабо связанных валидационных описаний.

Библиотека Yup предоставляет гибкий механизм композиции схем, что делает её удобной основой для доменной архитектуры валидации.


Домен как единица ответственности

Домен в контексте схем Yup — это логически изолированная область данных, отражающая конкретную бизнес-сущность:

  • пользователь (user)
  • аутентификация (auth)
  • платежи (billing)
  • профиль (profile)
  • настройки (settings)

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

Такое разделение обеспечивает:

  • снижение связности между частями схем
  • переиспользование валидаторов
  • упрощение тестирования
  • локализацию изменений

Базовая структура доменной схемы

На уровне файловой организации каждая доменная схема выделяется в отдельный модуль.

Пример структуры:

schemas/
  user.schema.js
  auth.schema.js
  profile.schema.js
  billing.schema.js

Каждый файл экспортирует либо отдельные поля, либо целую схему.


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

Схема пользователя часто становится центральной в приложении, но при правильной декомпозиции она не превращается в монолит.

import * as yup from "yup";

export const userBaseSchema = yup.object({
  id: yup.string().uuid().required(),
  email: yup.string().email().required(),
  createdAt: yup.date().required(),
});

Эта схема описывает только базовые свойства сущности, не включая бизнес-логику других доменов.


Расширение домена через композицию

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

import { userBaseSchema } from "./user.schema";
import * as yup from "yup";

export const userProfileSchema = userBaseSchema.concat(
  yup.object({
    firstName: yup.string().min(2).required(),
    lastName: yup.string().min(2).required(),
    avatarUrl: yup.string().url().nullable(),
  })
);

Такой подход позволяет:

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

Изоляция домена аутентификации

Домен аутентификации обычно не должен зависеть от бизнес-сущностей приложения.

import * as yup from "yup";

export const loginSchema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
});

export const registerSchema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
  confirmPassword: yup
    .string()
    .oneOf([yup.ref("password")], "Пароли должны совпадать")
    .required(),
});

Здесь домен изолирован и не использует внешние схемы, что снижает риск циклических зависимостей.


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

Одним из ключевых преимуществ Yup является возможность выделения переиспользуемых фрагментов.

export const emailField = yup.string().email().required();
export const passwordField = yup.string().min(8).required();

Далее они используются в разных доменах:

import { emailField, passwordField } from "../common/fields";

export const authSchema = yup.object({
  email: emailField,
  password: passwordField,
});

Такой подход превращает схемы в набор композиционных блоков.


Разделение по уровням абстракции

При росте системы домены дополнительно делятся на уровни:

  • базовые поля (fields)
  • сущности (entities)
  • формы (forms)
  • API DTO (data transfer objects)

Пример слоистой организации

validation/
  fields/
  entities/
  forms/
  dto/

Каждый уровень использует предыдущий, но не наоборот.


Использование lazy для сложных доменных зависимостей

В случаях, когда домены взаимно ссылаются друг на друга, применяется lazy.

import * as yup from "yup";

export const commentSchema = yup.object({
  id: yup.string().required(),
  text: yup.string().required(),
  author: yup.lazy(() => userBaseSchema),
});

lazy позволяет избежать циклического импорта и откладывает вычисление схемы до момента выполнения.


Разделение схем по контексту использования

Один и тот же домен может иметь разные представления в зависимости от контекста:

  • создание сущности
  • обновление
  • отображение

Пример для профиля:

export const profileCreateSchema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
});

export const profileUpdateSchema = yup.object({
  firstName: yup.string(),
  lastName: yup.string(),
  avatarUrl: yup.string().url(),
});

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


Композиция доменов в верхнеуровневую схему

На уровне API часто требуется объединение нескольких доменов.

import { userBaseSchema } from "./user.schema";
import { profileSchema } from "./profile.schema";

export const userFullSchema = userBaseSchema.concat(profileSchema);

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


Инкапсуляция бизнес-правил внутри домена

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

export const billingSchema = yup.object({
  cardNumber: yup
    .string()
    .matches(/^\d{16}$/, "Неверный номер карты")
    .required(),
  expiryDate: yup.date().required(),
});

Бизнес-логика платежей не должна проникать в домен пользователя или профиля.


Принцип независимого тестирования схем

Разделение по доменам позволяет тестировать каждую схему изолированно.

test("auth schema rejects short password", async () => {
  await expect(
    loginSchema.validate({
      email: "test@mail.com",
      password: "123",
    })
  ).rejects.toThrow();
});

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


Ошибки при отсутствии доменной декомпозиции

При отсутствии разделения схем возникают типичные проблемы:

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

Особенно критично это проявляется в формах с большим количеством полей из разных бизнес-областей.


Организация зависимостей между доменами

Корректная архитектура предполагает однонаправленные зависимости:

fields → entities → forms → dto

Нарушение этого порядка приводит к:

  • циклическим импортам
  • нестабильной структуре проекта
  • усложнению рефакторинга

Локализация изменений внутри домена

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

Пример: изменение формата email влияет только на fields/email, а не на десятки схем в разных частях приложения.


Масштабирование доменной архитектуры

При росте системы домены могут дробиться дальше:

  • user → user-public, user-admin
  • billing → billing-subscription, billing-invoice

Yup сохраняет гибкость за счёт возможности композировать схемы без изменения исходных блоков.


Контроль границ доменов

Для поддержания архитектурной чистоты важно фиксировать границы:

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

Итоговая модель взаимодействия доменов

В зрелой архитектуре схема валидации перестаёт быть единым объектом и превращается в систему независимых модулей, взаимодействующих через композицию и переиспользование.

Yup в таком подходе выступает не просто библиотекой проверки данных, а инструментом построения структурированной модели данных на уровне приложения.