Синхронизация валидации между клиентской и серверной частью решает проблему расхождения бизнес-правил, когда форма на клиенте принимает одни ограничения, а сервер — другие. В экосистеме React и форм-менеджмента на основе схем часто используется связка схем валидации и резолверов, где Yup выступает единым источником описания правил, а YupResolver обеспечивает их выполнение в рамках клиентской формы.
Центральная идея синхронизации заключается в том, чтобы не дублировать правила валидации в двух независимых слоях. Вместо этого создаётся единая схема:
import * as Yup from "yup";
const userSchema = Yup.object({
email: Yup.string()
.required("Email обязателен")
.email("Некорректный формат email"),
password: Yup.string()
.required("Пароль обязателен")
.min(8, "Минимум 8 символов"),
age: Yup.number()
.min(18, "Доступ только с 18 лет")
.max(100, "Некорректный возраст"),
});
Такая схема используется как на клиенте, так и на сервере, либо её часть транслируется в серверную проверку. Это снижает вероятность расхождений и упрощает сопровождение.
YupResolver выступает адаптером между схемой Yup и механизмом валидации форм, например в react-hook-form. Его задача — преобразовать синхронную или асинхронную проверку Yup в формат, понятный системе управления формой.
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
const form = useForm({
resolver: yupResolver(userSchema),
});
В этом контексте YupResolver обеспечивает:
Важно, что клиентская валидация не заменяет серверную, а лишь дублирует её для повышения UX.
Проблема синхронизации возникает из-за различий в средах выполнения:
Типичный разрыв проявляется в следующих сценариях:
YupResolver закрывает только первый слой проблемы — он делает клиентскую валидацию согласованной с описанной схемой, но не решает серверную часть напрямую.
Существует три основных подхода к синхронизации:
Схема Yup выносится в общий пакет:
/shared
validation/
userSchema.js
И используется на клиенте и сервере:
// client
import { userSchema } from "@shared/validation";
// server
import { userSchema } from "@shared/validation";
Этот подход минимизирует расхождения, но требует строгой архитектурной дисциплины.
Сервер не использует Yup напрямую, но реализует эквивалентные правила:
if (password.length < 8) {
errors.push("PASSWORD_TOO_SHORT");
}
Клиентская схема Yup при этом отображает эти ошибки в человекочитаемом виде.
Сервер выполняет полную валидацию и возвращает структурированные ошибки:
{
"fieldErrors": {
"email": "EMAIL_ALREADY_EXISTS"
}
}
Клиент сопоставляет их с Yup-схемой или отдельным маппингом.
YupResolver по умолчанию работает только с Yup-схемой, однако в реальных приложениях необходимо объединять клиентскую и серверную валидацию.
Типичный поток выглядит так:
Пример обработки серверных ошибок:
try {
await api.createUser(data);
} catch (error) {
setError("email", {
type: "server",
message: "Email уже используется",
});
}
Таким образом, YupResolver отвечает за первый слой, а серверные ошибки дополняют его вторым слоем валидации.
Основная проблема синхронизации возникает при наличии двух источников правил:
Если они расходятся, появляются трудноуловимые баги. Например:
Решение заключается не в расширении YupResolver, а в архитектурной унификации правил.
Yup поддерживает асинхронные тесты, что позволяет частично перенести серверную проверку в клиент:
email: Yup.string()
.email()
.test("check-email", "Email занят", async (value) => {
const res = await api.checkEmail(value);
return res.available;
})
При использовании с YupResolver это превращается в прозрачную часть процесса валидации формы.
Однако такой подход имеет ограничения:
Поэтому асинхронные проверки применяются точечно: уникальность, доступность ресурсов, ограничения по бизнес-правилам.
Сервер и клиент часто используют разные структуры ошибок. Для синхронизации требуется слой нормализации.
Пример серверного ответа:
{
"errors": [
{ "field": "password", "code": "TOO_WEAK" }
]
}
Маппер на клиенте:
const mapServerErrors = (errors) => {
return errors.reduce((acc, err) => {
acc[err.field] = translateError(err.code);
return acc;
}, {});
};
После этого ошибки передаются в форму, где YupResolver уже завершил свою часть работы, а серверные ошибки отображаются поверх него.
В системе с YupResolver и серверной проверкой важно определять приоритет источников ошибок:
При конфликте серверная ошибка всегда имеет приоритет, так как она отражает текущее состояние системы.
Не всегда возможно использовать одну и ту же схему на клиенте и сервере. В таких случаях применяется частичная синхронизация:
Пример:
const baseSchema = {
email: Yup.string().email().required(),
};
const clientSchema = Yup.object({
...baseSchema,
password: Yup.string().min(8),
});
Сервер может расширять эти правила без изменения клиентской логики.
В реальных системах правила валидации меняются со временем. Синхронизация должна учитывать:
Типичный подход:
Даже при идеальной синхронизации возникают временные расхождения:
Такие состояния неизбежны и компенсируются стратегиями:
Несмотря на удобство, YupResolver не решает фундаментальные задачи синхронизации:
Его роль ограничена адаптацией Yup-схемы к системе управления формами, тогда как архитектурная синхронизация остаётся задачей уровня приложения.
В сложных системах используется комбинированный подход:
Такая модель обеспечивает баланс между UX и консистентностью данных, минимизируя расхождения между слоями без избыточного дублирования логики.