Установка и начальная настройка

Vest — библиотека декларативной валидации данных для JavaScript и TypeScript, ориентированная на создание сложных сценариев проверки форм. Основная идея Vest заключается в построении тестовых наборов (suite), напоминающих структуру модульных тестов. Вместо описания правил через конфигурационные объекты используется программный подход с условиями, группировкой и управлением потоком выполнения.

Vest особенно удобна в следующих сценариях:

  • большие формы с множеством взаимозависимых полей;
  • динамическая валидация;
  • асинхронные проверки;
  • многошаговые формы;
  • сложная логика условий;
  • интеграция с React, Vue, Angular и обычным JavaScript.

Установка Vest

Установка через npm

npm install vest

Установка через yarn

yarn add vest

Установка через pnpm

pnpm add vest

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

npm

npm list vest

yarn

yarn list vest

Подключение библиотеки

CommonJS

const vest = require('vest');

ES Modules

Современный вариант подключения:

import { create, test, enforce } from 'vest';

Наиболее часто используются:

Функция Назначение
create создание набора проверок
test объявление отдельного теста
enforce встроенные правила проверки
only запуск конкретного поля
skip пропуск проверок
omitWhen условное исключение
warn предупреждения вместо ошибок

Минимальная структура проекта

project/
├── src/
│   ├── validation/
│   │   └── userValidation.js
│   ├── forms/
│   └── app.js
├── package.json
└── node_modules/

Первая настройка валидатора

Базовый пример

import { create, test, enforce } from 'vest';

export const userSuite = create((data = {}) => {

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^\S+@\S+\.\S+$/);
  });

  test('password', 'Минимум 8 символов', () => {
    enforce(data.password).longerThanOrEquals(8);
  });

});

Принцип работы create

Функция create формирует набор тестов — validation suite.

const suite = create((data) => {

});

Аргументом выступает callback, внутри которого размещаются проверки.

При вызове:

suite(formData);

Vest:

  1. выполняет все тесты;
  2. сохраняет состояние;
  3. собирает ошибки;
  4. отслеживает изменения;
  5. возвращает объект результата.

Выполнение проверки

const result = userSuite({
  email: 'admin@mail.com',
  password: '12345678'
});

Получение ошибок

Проверка наличия ошибок

result.hasErrors();

Ошибки конкретного поля

result.getErrors('email');

Все ошибки

result.getErrors();

Пример результата

{
  email: [
    'Некорректный email'
  ],
  password: [
    'Минимум 8 символов'
  ]
}

Функция test

Каждая проверка описывается через test.

Сигнатура:

test(fieldName, message, callback);

Пример

test('username', 'Имя обязательно', () => {
  enforce(data.username).isNotBlank();
});

Параметры

Параметр Описание
fieldName имя поля
message текст ошибки
callback логика проверки

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

enforce — встроенный механизм assertions.

Проверка длины

enforce(value).longerThan(3);

Проверка диапазона

enforce(age).greaterThanOrEquals(18);

Проверка строки

enforce(name).isNotBlank();

Проверка массива

enforce(items).isArray();

Проверка типа

enforce(value).isString();

Наиболее используемые проверки

Метод Назначение
isNotBlank() строка не пустая
isString() строка
isNumber() число
isArray() массив
isBoolean() boolean
longerThan() длина больше
shorterThan() длина меньше
equals() равенство
inside() наличие в массиве
matches() регулярное выражение

Валидация формы регистрации

import { create, test, enforce } from 'vest';

export const registrationSuite = create((data = {}) => {

  test('login', 'Логин обязателен', () => {
    enforce(data.login).isNotBlank();
  });

  test('login', 'Минимум 4 символа', () => {
    enforce(data.login).longerThanOrEquals(4);
  });

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^\S+@\S+\.\S+$/);
  });

  test('password', 'Минимум 8 символов', () => {
    enforce(data.password).longerThanOrEquals(8);
  });

});

Несколько тестов для одного поля

Vest позволяет назначать несколько проверок одному полю.

test('password', 'Введите пароль', () => {
  enforce(data.password).isNotBlank();
});

test('password', 'Минимум 8 символов', () => {
  enforce(data.password).longerThanOrEquals(8);
});

test('password', 'Нужна цифра', () => {
  enforce(data.password).matches(/\d/);
});

Проверка совпадения паролей

test('confirmPassword', 'Пароли не совпадают', () => {
  enforce(data.confirmPassword).equals(data.password);
});

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

Пример с флагом

test('companyName', 'Введите название компании', () => {

  if (!data.isBusiness) {
    return;
  }

  enforce(data.companyName).isNotBlank();
});

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

test('middleName', 'Слишком короткое отчество', () => {

  if (!data.middleName) {
    return;
  }

  enforce(data.middleName).longerThan(2);
});

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

only запускает проверки только указанного поля.

Импорт

import { only } from 'vest';

Пример

const suite = create((data, fieldName) => {

  only(fieldName);

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^\S+@\S+\.\S+$/);
  });

  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThanOrEquals(8);
  });

});

Вызов

suite(formData, 'email');

Выполнятся только проверки email.


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

skip временно отключает проверки.

import { skip } from 'vest';

skip(
  data.isAdmin,
  () => {

    test('role', 'Роль обязательна', () => {
      enforce(data.role).isNotBlank();
    });

  }
);

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

omitWhen полностью исключает проверки из выполнения.

import { omitWhen } from 'vest';

omitWhen(data.isGuest, () => {

  test('phone', 'Телефон обязателен', () => {
    enforce(data.phone).isNotBlank();
  });

});

Предупреждения через warn

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

import { warn } from 'vest';

test('password', 'Слабый пароль', () => {

  warn();

  enforce(data.password).matches(/[A-Z]/);
});

Проверка предупреждений

result.hasWarnings();
result.getWarnings();

Асинхронная валидация

Vest поддерживает async/await.

Проверка уникальности email

test(
  'email',
  'Email уже используется',
  async () => {

    const response = await fetch('/api/check-email');
    const result = await response.json();

    enforce(result.available).isTruthy();
  }
);

Асинхронный suite

const result = await registrationSuite(data);

Обработка ошибок API

test(
  'username',
  'Имя уже занято',
  async () => {

    try {

      const response = await fetch('/api/check-username');
      const result = await response.json();

      enforce(result.available).isTruthy();

    } catch (e) {

      throw new Error('Ошибка проверки');

    }

  }
);

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

Установка React-версии

npm install vest vest-utils

Пример React-валидации

import { useState } from 'react';
import { create, test, enforce, only } from 'vest';

const suite = create((data, fieldName) => {

  only(fieldName);

  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^\S+@\S+\.\S+$/);
  });

});

function App() {

  const [form, setForm] = useState({
    email: ''
  });

  const [errors, setErrors] = useState({});

  const validate = (fieldName) => {

    const result = suite(form, fieldName);

    setErrors(result.getErrors());
  };

  return (
    <input
      value={form.email}
      onCha nge={(e) => {
        setForm({
          ...form,
          email: e.target.value
        });

        validate('email');
      }}
    />
  );
}

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

Установка типов

Vest содержит встроенную поддержку TypeScript.

Дополнительные пакеты обычно не требуются.


Типизация данных формы

interface RegistrationForm {
  email: string;
  password: string;
}

Типизированный suite

import { create, test, enforce } from 'vest';

const suite = create((data: RegistrationForm) => {

  test('email', 'Некорректный email', () => {
    enforce(data.email).isString();
  });

});

Организация validation-слоя

Крупные проекты обычно разделяют проверки по модулям.

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

validation/
├── auth/
│   ├── loginSuite.js
│   └── registrationSuite.js
├── profile/
│   └── profileSuite.js
└── shared/
    └── rules.js

Вынос общих правил

export const emailRule = (value) => {
  enforce(value).matches(/^\S+@\S+\.\S+$/);
};

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

import { emailRule } from './rules';

test('email', 'Некорректный email', () => {
  emailRule(data.email);
});

Рекомендации по начальной настройке

Разделение правил

Хорошая практика — хранить:

  • suite отдельно;
  • API-проверки отдельно;
  • regex-шаблоны отдельно;
  • общие правила отдельно.

Единый формат сообщений

Нежелательно смешивать разные стили:

'Поле обязательно'
'Введите значение'
'Error'

Лучше придерживаться единого формата:

'Введите email'
'Введите пароль'
'Минимум 8 символов'

Минимизация логики внутри test

Плохо:

test('email', 'Ошибка', () => {

  if (
    data.email &&
    data.email.length > 5 &&
    /^\S+@\S+\.\S+$/.test(data.email)
  ) {

  }

});

Лучше:

test('email', 'Некорректный email', () => {
  enforce(data.email).matches(/^\S+@\S+\.\S+$/);
});

Частые ошибки при настройке Vest

Отсутствие only

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

only(fieldName);

Смешивание UI и валидации

Нежелательно:

test('email', 'Ошибка', () => {

  setState();

  enforce(data.email).isNotBlank();

});

Vest должен отвечать только за проверки.


Большие suites

Плохо:

validation.js

на 2000 строк.

Лучше:

validation/
  auth/
  profile/
  settings/

Отладка validation suites

Логирование результата

const result = suite(data);

console.log(result.getErrors());

Проверка конкретного поля

console.log(result.getErrors('email'));

Подготовка production-конфигурации

Для production-проектов обычно добавляют:

  • централизованные правила;
  • локализацию сообщений;
  • async API validators;
  • debounce-проверки;
  • разделение client/server validation;
  • unit-тестирование validation suites.

Unit-тестирование suites

Vest хорошо сочетается с:

  • Jest
  • Vitest
  • Mocha

Пример теста

test('email validation', () => {

  const result = suite({
    email: ''
  });

  expect(result.hasErrors('email')).toBe(true);
});