Организация кода валидации

Организация кода валидации в Vest строится вокруг идеи изолированных наборов правил, объединённых в логические блоки. Базовой единицей выступает suite, внутри которой группируются тесты test, а сами проверки реализуются через enforce.

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

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

// validation/user.validation.js
import { suite, test, enforce } from 'vest';

export const userValidation = suite('user', (data = {}) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });

  test('password', 'weak_password', () => {
    enforce(data.password).longerThan(6);
  });
});

Такое разделение позволяет локализовать изменения и снижает связанность кода.


Разделение доменов и форм

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

Подход:

  • auth.validation.js — регистрация, логин, восстановление пароля
  • profile.validation.js — пользовательские данные
  • billing.validation.js — платёжные данные
// validation/auth.validation.js
export const loginValidation = suite('login', (data) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });

  test('password', 'required', () => {
    enforce(data.password).isNotEmpty();
  });
});

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


Организация suite и вложенных наборов

В Vest допускается логическая композиция наборов валидации через функции и переиспользуемые блоки.

Структурирование через вложенные функции:

const emailRules = (data) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });
};

const passwordRules = (data) => {
  test('password', 'weak_password', () => {
    enforce(data.password).longerThan(6);
  });
};

export const registrationValidation = suite('registration', (data) => {
  emailRules(data);
  passwordRules(data);
});

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


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

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

// rules/email.rules.js
export const emailRules = (data) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });
};

Использование:

import { emailRules } from './rules/email.rules';

export const contactFormValidation = suite('contact', (data) => {
  emailRules(data);

  test('message', 'required', () => {
    enforce(data.message).isNotEmpty();
  });
});

Такой подход формирует библиотеку правил, применимую во множестве контекстов.


Фабрики правил

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

const createLengthRule = (field, min) => (data) => {
  test(field, 'too_short', () => {
    enforce(data[field]).longerThan(min);
  });
};

const min8CharsPassword = createLengthRule('password', 8);

export const securityValidation = suite('security', (data) => {
  min8CharsPassword(data);
});

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


Композиция и декомпозиция

При сложных формах валидация должна быть разложена на логические слои:

  • базовые поля
  • зависимые поля
  • бизнес-ограничения
const baseUserFields = (data) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });
};

const profileConstraints = (data) => {
  test('username', 'too_short', () => {
    enforce(data.username).longerThan(3);
  });
};

export const userProfileValidation = suite('profile', (data) => {
  baseUserFields(data);
  profileConstraints(data);
});

Декомпозиция делает код линейным по сложности и уменьшает когнитивную нагрузку.


Асинхронные проверки и сайд-эффекты

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

test('username', 'username_taken', async () => {
  const exists = await api.checkUsername(data.username);
  enforce(exists).isFalsy();
});

Рекомендуемая структура:

  • синхронные правила — в базовых модулях
  • асинхронные — в отдельном слое
const asyncRules = (data) => {
  test('email', 'email_taken', async () => {
    const exists = await api.checkEmail(data.email);
    enforce(exists).isFalsy();
  });
};

Разделение предотвращает блокировку синхронной логики и упрощает тестирование.


Условная валидация

В сложных формах часть правил активируется только при определённых условиях.

test('companyName', 'required', () => {
  if (data.isCompany) {
    enforce(data.companyName).isNotEmpty();
  }
});

Для масштабируемости такие условия выносятся в отдельные функции:

const ifCompany = (data, fn) => {
  if (data.isCompany) fn();
};

export const checkoutValidation = suite('checkout', (data) => {
  ifCompany(data, () => {
    test('companyName', 'required', () => {
      enforce(data.companyName).isNotEmpty();
    });
  });
});

Централизация сообщений об ошибках

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

export const errorMessages = {
  invalid_email: 'Email имеет некорректный формат',
  weak_password: 'Пароль недостаточной длины',
};

Связка происходит по ключу ошибки, возвращаемому test.

Структурирование:

  • errors/ — словари сообщений
  • validation/ — логика проверки
  • constants/ — коды ошибок

Типизация и интеграция с TypeScript

При использовании TypeScript структура валидации становится строго типизированной, что снижает количество ошибок на этапе разработки.

type LoginData = {
  email: string;
  password: string;
};

export const loginValidation = suite<LoginData>('login', (data) => {
  test('email', 'invalid_email', () => {
    enforce(data.email).isEmail();
  });
});

Рекомендуется:

  • типизировать входные данные suite
  • избегать any
  • выносить типы в отдельный слой types/

Структура проекта

При росте приложения эффективна следующая организация:

src/
  validation/
    auth/
      login.validation.js
      register.validation.js
    profile/
      profile.validation.js
  rules/
    email.rules.js
    password.rules.js
  errors/
    auth.errors.js
    profile.errors.js
  types/
    auth.types.ts

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


Паттерны масштабирования

При увеличении количества форм и правил критически важно соблюдать несколько архитектурных принципов:

  • Изоляция доменов — каждая область независима
  • Композиция вместо наследования — правила собираются из функций
  • Минимизация дублирования — общие правила выносятся в библиотеки
  • Явные зависимости — каждая suite получает данные явно
  • Разделение синхронной и асинхронной логики

Эти принципы обеспечивают предсказуемость поведения системы валидации и упрощают её развитие при росте проекта