Миграция на Vest из других систем валидации обычно требует не прямого переписывания правил один-в-один, а переосмысления модели валидации. Vest построен вокруг концепции тестов, сгруппированных в “сессии”, где каждая проверка — это отдельное утверждение. Это отличается от декларативных схем, используемых в библиотеках вроде Yup или Joi.
Ключевая особенность подхода:
Такой подход влияет на архитектуру миграции: переносится не схема, а логика проверки.
Большинство популярных библиотек используют схемы:
Vest использует другую модель:
Пример различия:
Yup:
const schema = yup.object({
email: yup.string().email().required()
});
Vest:
import { create, test, enforce } from 'vest';
const validate = create((data) => {
test('email', 'Invalid email', () => {
enforce(data.email).isEmail();
});
test('email', 'Email is required', () => {
enforce(data.email).isNotEmpty();
});
});
При миграции важно учитывать: порядок тестов и их группировка становятся частью логики, а не побочным эффектом.
Yup часто используется в React-проектах, особенно вместе с формами. Основная сложность перехода — замена декларативных схем на процедурные тесты.
| Yup | Vest |
|---|---|
| string().required() | test + enforce.isNotEmpty() |
| email() | enforce.isEmail() |
| min(n) | enforce.isGte(n.length) |
| when() | условные test() |
Yup:
const schema = yup.object({
password: yup.string().min(8).required(),
});
Vest:
import { create, test, enforce } from 'vest';
const validate = create((data) => {
test('password', 'Required', () => {
enforce(data.password).isNotEmpty();
});
test('password', 'Too short', () => {
enforce(data.password.length).greaterThanOrEquals(8);
});
});
Yup:
yup.string().when('role', {
is: 'admin',
then: schema => schema.required()
});
Vest:
test('code', 'Required for admin', () => {
if (data.role === 'admin') {
enforce(data.code).isNotEmpty();
}
});
Главное отличие: условия становятся частью обычного JS-кода, а не DSL.
Joi ориентирован на строгую структуру данных и часто используется в backend-валидации.
Joi:
const schema = Joi.object({
age: Joi.number().min(18).required()
});
Vest:
test('age', 'Required', () => {
enforce(data.age).isNotEmpty();
});
test('age', 'Must be at least 18', () => {
enforce(data.age).greaterThanOrEquals(18);
});
В Joi структура данных проверяется целиком. В Vest каждая проверка изолирована, что упрощает частичную валидацию (например, при вводе формы по полям).
Zod ближе всего к TypeScript и часто используется в типобезопасных приложениях.
Zod:
Vest:
Zod:
const schema = z.object({
username: z.string().min(3)
});
Vest:
test('username', 'Too short', () => {
enforce(data.username.length).greaterThanOrEquals(3);
});
При миграции с Zod важно учитывать:
Во многих библиотеках асинхронные проверки описываются отдельно (например, remote validation в Yup или Zod).
Vest поддерживает асинхронные тесты напрямую.
Yup (через кастомный test):
email: yup.string().test('unique', async (value) => {
return await api.checkEmail(value);
});
Vest:
test('email', 'Email already exists', async () => {
await enforce(data.email).matches(async (value) => {
return await api.checkEmail(value);
});
});
В схемных библиотеках часто используется вложенная структура:
В Vest структура упрощается до ключей и условий.
Yup:
address: yup.object({
city: yup.string().required()
});
Vest:
test('address.city', 'Required', () => {
enforce(data.address?.city).isNotEmpty();
});
Иерархия становится “плоской”, а вложенность выражается через строки ключей.
Списки в схемных библиотеках обычно описываются через array schemas.
Zod:
z.array(z.string().min(3))
Vest:
data.items.forEach((item, index) => {
test(`items.${index}`, 'Too short', () => {
enforce(item.length).greaterThanOrEquals(3);
});
});
Особенность: контроль индекса полностью на стороне разработчика.
При переходе с Formik, React Hook Form или аналогов меняется способ привязки ошибок.
Пример использования:
const result = validate(formData);
if (result.hasErrors('email')) {
console.log(result.getErrors('email'));
}
Formik обычно ожидает объект ошибок:
{
email: "Invalid email"
}
Vest возвращает агрегированное состояние, которое часто требует адаптера:
const errors = result.getErrors();
Код становится более императивным, что требует дисциплины в организации тестов.
Каждое правило — отдельный test, что увеличивает объем кода.
Нет единой структуры, которая описывает всю форму.
Порядок test влияет на UX (например, какая ошибка покажется первой).
Полный переход редко выполняется сразу. Используются гибридные подходы:
Можно временно использовать адаптер:
const legacyErrors = yupSchema.validateSync(data, { abortEarly: false });
const vestErrors = validate(data);
Обе системы работают одновременно, сравнивая результаты до полного перехода.
После перехода на Vest появляется возможность улучшить архитектуру:
Пример группировки:
const validateUser = (data) => {
test('user.name', ...);
test('user.email', ...);
};
const validateProfile = (data) => {
test('profile.age', ...);
};
Миграция требует перехода от “описания структуры данных” к “описанию поведения системы”.
Такой подход особенно эффективен в сложных формах с динамическими правилами, где схемные библиотеки начинают ограничивать гибкость.