Синхронизация клиентской и серверной валидации

Синхронизация валидации между клиентской и серверной частью решает проблему расхождения бизнес-правил, когда форма на клиенте принимает одни ограничения, а сервер — другие. В экосистеме 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 в клиентской валидации

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 обеспечивает:

  • выполнение схемы при изменении или сабмите формы
  • возврат ошибок в структуре, совместимой с formState.errors
  • поддержку асинхронной валидации (например, при проверке уникальности email через сервер)

Важно, что клиентская валидация не заменяет серверную, а лишь дублирует её для повышения UX.

Архитектурный разрыв между клиентом и сервером

Проблема синхронизации возникает из-за различий в средах выполнения:

  • клиент работает в браузере и оптимизирует UX
  • сервер работает в доверенной среде и обеспечивает безопасность
  • правила бизнес-логики могут эволюционировать независимо

Типичный разрыв проявляется в следующих сценариях:

  • клиент разрешает отправку формы, сервер отклоняет запрос
  • сервер добавляет новое правило, клиент о нём не знает
  • различается формат ошибок

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

Дублирование схем и стратегия синхронизации

Существует три основных подхода к синхронизации:

1. Общая схема в монорепозитории

Схема Yup выносится в общий пакет:

/shared
  validation/
    userSchema.js

И используется на клиенте и сервере:

// client
import { userSchema } from "@shared/validation";

// server
import { userSchema } from "@shared/validation";

Этот подход минимизирует расхождения, но требует строгой архитектурной дисциплины.

2. Производная серверная валидация

Сервер не использует Yup напрямую, но реализует эквивалентные правила:

if (password.length < 8) {
  errors.push("PASSWORD_TOO_SHORT");
}

Клиентская схема Yup при этом отображает эти ошибки в человекочитаемом виде.

3. Сервер как источник ошибок, клиент как интерпретатор

Сервер выполняет полную валидацию и возвращает структурированные ошибки:

{
  "fieldErrors": {
    "email": "EMAIL_ALREADY_EXISTS"
  }
}

Клиент сопоставляет их с Yup-схемой или отдельным маппингом.

Интеграция серверных ошибок в YupResolver поток

YupResolver по умолчанию работает только с Yup-схемой, однако в реальных приложениях необходимо объединять клиентскую и серверную валидацию.

Типичный поток выглядит так:

  1. клиент валидирует данные через YupResolver
  2. при успешной валидации отправляет запрос на сервер
  3. сервер выполняет собственную проверку
  4. при ошибке сервер возвращает structured response
  5. клиент маппит ошибки в форму

Пример обработки серверных ошибок:

try {
  await api.createUser(data);
} catch (error) {
  setError("email", {
    type: "server",
    message: "Email уже используется",
  });
}

Таким образом, YupResolver отвечает за первый слой, а серверные ошибки дополняют его вторым слоем валидации.

Конфликт источников истины

Основная проблема синхронизации возникает при наличии двух источников правил:

  • Yup-схема на клиенте
  • бизнес-логика на сервере

Если они расходятся, появляются трудноуловимые баги. Например:

  • Yup допускает значение null, сервер нет
  • сервер требует дополнительные поля, отсутствующие в схеме
  • различия в форматах дат и чисел

Решение заключается не в расширении YupResolver, а в архитектурной унификации правил.

Асинхронная валидация и серверные проверки

Yup поддерживает асинхронные тесты, что позволяет частично перенести серверную проверку в клиент:

email: Yup.string()
  .email()
  .test("check-email", "Email занят", async (value) => {
    const res = await api.checkEmail(value);
    return res.available;
  })

При использовании с YupResolver это превращается в прозрачную часть процесса валидации формы.

Однако такой подход имеет ограничения:

  • увеличивает количество запросов
  • создаёт зависимость UI от сети
  • усложняет контроль кеширования

Поэтому асинхронные проверки применяются точечно: уникальность, доступность ресурсов, ограничения по бизнес-правилам.

Нормализация ошибок между слоями

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

Пример серверного ответа:

{
  "errors": [
    { "field": "password", "code": "TOO_WEAK" }
  ]
}

Маппер на клиенте:

const mapServerErrors = (errors) => {
  return errors.reduce((acc, err) => {
    acc[err.field] = translateError(err.code);
    return acc;
  }, {});
};

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

Приоритеты валидации

В системе с YupResolver и серверной проверкой важно определять приоритет источников ошибок:

  1. клиентская Yup-валидация (быстрая обратная связь)
  2. серверная валидация (истинный источник правил)
  3. бизнес-валидация внешних систем

При конфликте серверная ошибка всегда имеет приоритет, так как она отражает текущее состояние системы.

Частичная синхронизация схем

Не всегда возможно использовать одну и ту же схему на клиенте и сервере. В таких случаях применяется частичная синхронизация:

  • общие базовые правила (формат email, длина строк)
  • серверные расширения (уникальность, доступы)
  • клиентские ограничения UX (например, live validation)

Пример:

const baseSchema = {
  email: Yup.string().email().required(),
};

const clientSchema = Yup.object({
  ...baseSchema,
  password: Yup.string().min(8),
});

Сервер может расширять эти правила без изменения клиентской логики.

Эволюция правил и обратная совместимость

В реальных системах правила валидации меняются со временем. Синхронизация должна учитывать:

  • старые версии клиентов
  • новые серверные ограничения
  • постепенную миграцию схем

Типичный подход:

  • сервер возвращает версию ошибки
  • клиент интерпретирует её в зависимости от версии схемы
  • YupResolver остаётся неизменным, так как работает только с текущей схемой

Пограничные состояния и UX-расхождения

Даже при идеальной синхронизации возникают временные расхождения:

  • пользователь ввёл значение, клиент его принял
  • сервер обновил правила между отправкой и ответом
  • сеть внесла задержку между слоями проверки

Такие состояния неизбежны и компенсируются стратегиями:

  • повторная валидация перед отправкой
  • optimistic UI с откатом
  • централизованная обработка ошибок формы

Ограничения подхода с YupResolver

Несмотря на удобство, YupResolver не решает фундаментальные задачи синхронизации:

  • не управляет серверной логикой
  • не гарантирует идентичность правил
  • не синхронизирует версии схем автоматически

Его роль ограничена адаптацией Yup-схемы к системе управления формами, тогда как архитектурная синхронизация остаётся задачей уровня приложения.

Комбинированные модели валидации

В сложных системах используется комбинированный подход:

  • YupResolver для клиентской схемы
  • серверная JSON-валидация
  • слой нормализации ошибок
  • общий пакет типов и схем

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