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

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

Принципы структурирования схем

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

Ключевые принципы:

  • Изоляция схем от UI-слоя Логика валидации не привязывается к конкретным компонентам интерфейса. Это снижает связанность и упрощает переиспользование.

  • Переиспользуемость фрагментов Общие правила (например, email, пароль, телефон) выносятся в отдельные базовые схемы.

  • Композиционность Сложные схемы собираются из более мелких блоков.

  • Единая точка определения правил Каждое правило существует в одном месте, исключая расхождения между формами.

Структура каталогов в проекте

При масштабировании проекта структура папок играет критическую роль. Типичная организация может выглядеть следующим образом:

src/
  validation/
    schemas/
      auth/
        login.schema.js
        register.schema.js
      user/
        profile.schema.js
        settings.schema.js
    base/
      string.schema.js
      number.schema.js
      date.schema.js
    shared/
      email.schema.js
      password.schema.js
      phone.schema.js
    index.js

Такое разделение позволяет:

  • быстро находить нужную схему;
  • разделять доменные области (auth, user и т.д.);
  • централизовать общие правила в shared.

Базовые схемы и их роль

Базовые схемы представляют собой строительные блоки, из которых формируются более сложные структуры. Они определяют минимальные ограничения и часто используются как основа для расширения.

Пример базового типа строки:

import * as Yup from 'yup';

export const baseString = Yup.string()
  .trim()
  .strict(true);

Расширение базовой схемы:

import { baseString } from '../base/string.schema';

export const emailSchema = baseString
  .email('Некорректный формат email')
  .required('Email обязателен');

Такой подход снижает дублирование и обеспечивает единообразие поведения.

Переиспользуемые доменные схемы

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

Пример схемы пароля:

import * as Yup from 'yup';

export const passwordSchema = Yup.string()
  .min(8, 'Минимум 8 символов')
  .matches(/[A-Z]/, 'Должна быть хотя бы одна заглавная буква')
  .matches(/[0-9]/, 'Должна быть хотя бы одна цифра')
  .required('Пароль обязателен');

Использование в разных формах:

import { passwordSchema } from '../shared/password.schema';

export const registerSchema = Yup.object({
  password: passwordSchema,
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password')], 'Пароли не совпадают')
});

Композиция схем

Одной из сильных сторон Yup является возможность комбинировать схемы. Это особенно важно при построении сложных объектов.

import * as Yup from 'yup';
import { emailSchema } from '../shared/email.schema';
import { passwordSchema } from '../shared/password.schema';

export const userSchema = Yup.object({
  email: emailSchema,
  password: passwordSchema
});

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

const baseUser = Yup.object({
  email: emailSchema
});

const extendedUser = baseUser.concat(
  Yup.object({
    role: Yup.string().required()
  })
);

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

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

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

Пример доменной структуры:

  • auth — авторизация и регистрация
  • user — профиль пользователя
  • billing — платежные данные
  • admin — административные сущности

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

Условная логика в схемах

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

import * as Yup from 'yup';

export const deliverySchema = Yup.object({
  hasAddress: Yup.boolean(),
  address: Yup.string().when('hasAddress', {
    is: true,
    then: schema => schema.required('Адрес обязателен'),
    otherwise: schema => schema.notRequired()
  })
});

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

Кросс-полевые проверки

Некоторые правила требуют доступа сразу к нескольким полям. В таких случаях используется test на уровне объекта.

export const paymentSchema = Yup.object({
  amount: Yup.number().required(),
  balance: Yup.number().required()
}).test(
  'balance-check',
  'Недостаточно средств',
  function (values) {
    return values.balance >= values.amount;
  }
);

Такие проверки следует выносить в отдельные функции для повторного использования.

Централизация схем

При увеличении количества схем полезно вводить единый экспортный модуль:

export { loginSchema } from './auth/login.schema';
export { registerSchema } from './auth/register.schema';
export { profileSchema } from './user/profile.schema';

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

import { loginSchema } from '@/validation';

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

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

Пример теста:

import { emailSchema } from './email.schema';

test('валидный email проходит проверку', async () => {
  await expect(emailSchema.validate('test@mail.com')).resolves.toBeDefined();
});

test('невалидный email вызывает ошибку', async () => {
  await expect(emailSchema.validate('invalid')).rejects.toThrow();
});

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

Интеграция с TypeScript

При использовании TypeScript схемы Yup могут быть связаны с типами данных, что снижает расхождение между валидацией и типизацией.

import * as Yup from 'yup';

export const userSchema = Yup.object({
  email: Yup.string().email().required(),
  age: Yup.number().required()
});

export type User = Yup.InferType<typeof userSchema>;

Это позволяет синхронизировать модель данных и правила валидации.

Ошибки архитектуры схем

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

  • дублирование одинаковых правил в разных формах;
  • размещение схем внутри компонентов;
  • отсутствие централизованных базовых схем;
  • чрезмерная вложенность when;
  • смешивание бизнес-логики и UI-логики.

Устранение этих проблем требует дисциплинированного подхода к структуре проекта.

Масштабирование системы схем

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

  • выделение доменных модулей;
  • использование базовых схем;
  • композиция через object и concat;
  • централизованный экспорт;
  • стандартизация именования.

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