Zod, Valibot и Yup для форм и API-границ в реальных приложениях
Сравните Zod, Valibot и Yup по размеру бандла, общим схемам и обработке ошибок для форм и API, чтобы выбрать подходящий вариант.

Почему этот выбор быстро становится сложным
Сначала это кажется небольшим решением о библиотеке. Выбираешь валидатор, пишешь несколько правил и идёшь дальше. Но в реальном приложении Zod, Valibot и Yup подталкивают к разным компромиссам: скорость разработки, общие схемы, размер бандла и обработка ошибок.
Формам нужна быстрая и понятная обратная связь. Если человек ошибся в адресе электронной почты, он ждёт полезное сообщение сразу. У API другая задача. Оно должно отклонять плохие данные всегда — независимо от того, пришли они из старого мобильного приложения, сломанного скрипта или бота, который шлёт мусор на ваш эндпоинт.
Потом команда просит одну схему, которая работает везде. Это часто помогает. Меньше расхождений, меньше дублирования правил. Но браузеру нужны дружелюбные сообщения и лёгкий код, а серверу — более строгие проверки и предсказуемый результат. Эти требования не всегда совпадают.
Чаще всего от одного выбора хотят пять вещей: быструю обратную связь в формах, строгую проверку API, одну общую схему, небольшой клиентский бандл и ошибки, которые помогают и пользователям, и разработчикам. Обычно можно получить большую часть этого. Но всё сразу — уже сложнее.
Чем отличаются Zod, Valibot и Yup
Главное отличие не в популярности. Оно в том, как каждая библиотека ощущается, когда вы пишете схемы, позже меняете их и используете снова в формах и обработчиках API.
Zod хорошо подходит для проектов на TypeScript. Синтаксис схем прямой, а выводимые типы обычно совпадают с тем, чего ждут разработчики. Если ваше приложение использует TypeScript и на фронтенде, и на бэкенде, Zod часто оказывается самым простым вариантом по умолчанию. Рефакторить с ним тоже обычно спокойнее. Переименовать поле, разбить объект, ужесточить правило — схема и типы, как правило, остаются рядом.
Valibot предлагает более компактный и модульный подход. Вы собираете валидацию из небольших функций, а не опираетесь на один большой API. Некоторым командам это нравится, потому что всё остаётся явным и, если нужно лишь несколько частей, его проще держать вне клиентского бандла. Другим кажется, что для типовых случаев он чуть более многословный.
Yup до сих пор встречается во многих старых React-стэках для форм, особенно в приложениях, которые выросли вокруг Formik. Если команда уже его знает и текущая схема работы устраивает, оставить его — вполне разумный выбор. Чаще всего трение появляется в TypeScript-коде. Общие схемы и вывод типов нередко требуют больше внимания, чем с Zod.
Если вы начинаете с нуля и используете TypeScript, Zod обычно самый безопасный выбор по умолчанию. Если размер браузерного кода критичен, Valibot стоит внимательно рассмотреть. Если Yup уже работает в продакшене и не создаёт проблем, не стоит мигрировать только потому, что появились более новые инструменты.
Размер бандла и то, что пользователи реально скачивают
На публичной странице лишний JavaScript заметен быстро. Форме регистрации на маркетинговом сайте совсем немного места для дополнительного кода, особенно на старых телефонах. Внутренняя админка обычно может позволить себе больше, потому что туда заходят реже и ожидают более тяжёлые инструменты.
Не смотрите на размер установки и обещания на странице пакета. Важно то, что попадает в итоговый клиентский бандл. Tree shaking, адаптеры форм, мапперы ошибок, обёртки и общие утилиты меняют реальную цифру. Команды иногда меняют библиотеку ради нескольких килобайт, а потом теряют экономию на дополнительном интеграционном коде.
Серверная валидация меняет расчёт. Если проверка идёт только в API-обработчиках или server actions, пользователи вообще не скачивают эту библиотеку. В таком случае важнее могут быть скорость разработки и понятный вывод ошибок, чем размер в браузере. Общие схемы всё ещё полезны, но неосторожные импорты могут подтянуть серверный код на клиент и свести пользу к нулю.
Простой пример хорошо показывает компромисс. Форма подписки на лендинге чувствует каждый лишний байт. Экран с выставлением счетов и более сложными правилами может больше ценить общие схемы и меньше ошибок, чем экономию последнего кусочка JavaScript.
Прежде чем менять библиотеку, измерьте своё приложение. Сравните итоговый бандл, протестируйте страницу, где валидация запускается чаще всего, и включите в проверку код адаптера. Репутация библиотеки — слабая замена тому, что на самом деле скачивают ваши пользователи.
Как использовать одну схему в формах и на границе API
Одна общая схема может убрать много тихих багов. Вы один раз описываете основные правила данных, а затем используете те же имена полей и те же проверки структуры и в форме, и на границе API. Это помогает избежать классической путаницы, когда браузер принимает значение, а сервер через несколько секунд его отклоняет.
Общей должна быть именно логика данных, а не весь пользовательский опыт. Форма может говорить: «Пожалуйста, введите email». API может писать в лог: «email должен быть валидным адресом». Правило одно, задачи разные. Экран должен помочь человеку быстро исправить поле. Сервер должен вернуть стабильную причину, которую разработчики смогут отследить.
Преобразования требуют того же разделения. Обрезать пробелы в поле email в браузере — нормально. Тихое приведение типов на границе API рискованнее, потому что может скрыть плохие запросы и усложнить отладку. Хороший подход — разделить базовую схему и тонкие обёртки для каждой стороны. Пусть форма будет чуть более снисходительной. Сервер — строгим.
Проверяйте неудобные значения заранее. Пустые строки, числа из текстовых полей, сюрпризы с часовыми поясами в датах, null вместо undefined, а также массивы, которые иногда приходят с одним элементом, а иногда совсем без него, создают больше проблем, чем команды ожидают.
Представьте поле возраста. Браузер получает \"18\" как текст, потому что так работают HTML-поля. Форма может преобразовать его в число ради удобства. Но на границе API всё равно нужно проверить, что итоговое значение — это реальное число в допустимом диапазоне, а не \"18 \", не \"18.0abc\" и не пустая строка, которая проскочила дальше.
Как ошибки попадают на экран и в логи
Ошибки валидации одновременно решают две задачи. Они подсказывают человеку, что исправить, и сообщают приложению, что сломалось. Держите эти результаты отдельно.
На экране лучше работают короткие сообщения. «Введите корректный email» — достаточно. API-клиентам нужен стабильный путь или код, например customer.email и invalid_email. Логам нужны исходные данные, правило, на котором всё сломалось, и полный объект ошибки. Если пытаться одним форматом обслужить все три аудитории, кто-то обязательно проиграет.
Zod обычно хорошо подходит для такого разделения, потому что его issues уже содержат понятные пути и сообщения. Valibot тоже возвращает структурированные ошибки и может быть удобнее, когда размер клиентского кода особенно важен. Yup во многих формах работает нормально, но команды часто тратят больше времени на преобразование его ошибок, прежде чем API-клиенты смогут на них полагаться.
Вложенные массивы быстро показывают слабое форматирование ошибок. Если items[2].price превращается в одно расплывчатое сообщение на всю строку, интерфейс становится запутанным, а логи перестают помогать. Практичный подход — хранить стабильные коды ошибок в схеме, сопоставлять эти коды коротким сообщениям для UI и отправлять сырое событие ошибки в логи.
Реалистичный пример приложения
Представьте страницу регистрации в SaaS с тремя полями: имя, email и размер компании. Форма выглядит маленькой, но затрагивает сразу три места. Браузеру нужна быстрая обратная связь, границе API нужно блокировать плохие данные, а админка позже может использовать те же данные с более строгими правилами.
В публичной форме пользователь должен видеть простые ошибки ещё до отправки. «Введите имя» — достаточно. «Используйте корректный email» — тоже достаточно. Размер бандла здесь важнее, чем многие признают. На офисном ноутбуке более тяжёлый валидатор может казаться нормальным. На старом телефоне с 4G страница начинает тормозить гораздо раньше.
Когда запрос доходит до API, правила должны оставаться строгими. Если имя отсутствует, email написан неверно или размер компании пустой, маршрут должен отклонить запрос в одном месте и вернуть ошибки по полям. Именно здесь помогают общие схемы. С Zod или Valibot многие команды описывают структуру один раз и используют её повторно. Yup тоже может хорошо работать в форме, но позже команды часто добавляют отдельную серверную валидацию, и это создаёт расхождения.
Теперь добавим админку. Сотрудники поддержки могут редактировать ту же запись, но им обычно нужны более строгие правила, чем на публичной странице регистрации. Например, публичная форма принимает \"1-10\", если пользователь ввёл его сам, а админский экран разрешает только фиксированный набор значений, например \"1-10\", \"11-50\" и \"51-200\". Базовая схема с небольшим расширением отлично это решает. Копировать и потом править вторую схему — обычно нет.
Ошибки, которые потом создают боль
Большинство проблем со схемами начинается незаметно. Поле работает в одной форме, ломается на границе API, и никто ничего не замечает, пока плохие данные не попадут в хранилище или логи не наполнятся бесполезными ошибками.
Одна частая ошибка — использовать разные правила приведения типов на клиенте и сервере. Браузер превращает \"42\" в число, а API ожидает строку и сначала обрезает её, или наоборот. Пользователь видит один результат, бэкенд — другой, и поддержка получает баги, которые трудно повторить.
Ещё одна ошибка — прятать все сбои за одним общим сообщением вроде «Неверный ввод». Сначала это кажется аккуратно, но потом замедляет всех. Пользователь не понимает, что исправить, а разработчики теряют детали, которые нужны в логах.
Копирование схем в каждую форму, маршрут API и тестовый файл приводит к более медленному, но коварному ущербу. Через несколько месяцев уже никто не знает, какая версия — настоящая. Общие схемы важны не потому, что выглядят красиво, а потому что останавливают тихое расхождение правил.
Преобразования тоже могут подставить команду. Если вы обрезаете, приводите тип, разделяете или перестраиваете данные до того, как прошли базовые проверки, можно превратить понятную ошибку в запутанное поведение. Сначала проверьте исходное значение. Преобразуйте его после того, как убедитесь, что оно соответствует ожидаемой форме.
Смена инструментов создаёт собственный хаос. Команда читает тренды, меняет библиотеку, обновляет адаптеры и только потом проверяет, что увидели пользователи. Сначала измеряйте, потом мигрируйте.
Как выбрать без лишних размышлений
Короткий эксперимент скажет правду быстрее, чем неделя обсуждений. Соберите одну реальную форму, один серверный обработчик и один вложенный payload. Повседневные неудобства быстро становятся видны, когда вы пробуете настоящую работу.
Начинайте с клиентского бандла, а не с README. Добавьте библиотеку на страницу, где уже есть код формы, и посмотрите, что попадёт в клиент. Потом проверьте совместное использование типов самым простым способом: напишите одну схему для чего-то обычного, используйте её в форме и на границе API и посмотрите, сколько вспомогательного кода вам пришлось написать.
Достаточно простого теста:
- Соберите одну реальную форму и один серверный обработчик с той же схемой.
- Измерьте итоговый клиентский бандл после импорта.
- Отправьте сломанные вложенные payload и посмотрите на ошибки UI, ответ API и логи.
- Посчитайте, сколько обёрток, адаптеров и кастомных мапперов вам понадобилось.
Вложенным массивам нужно уделять особое внимание. Плоская форма входа в аккаунт делает любую библиотеку простой. Корзина со строками товаров, скидками и адресами доставки — уже нет. Если там ошибки начинают путаться, код интерфейса становится шумным, а логи перестают помогать.
Моё простое правило такое: выбирайте Yup, если приложение уже от него зависит и цена миграции выше, чем текущая боль. Выбирайте Zod, если вам нужен самый безопасный вариант для общих схем и большой экосистемы. Выбирайте Valibot, если размер бандла критичен, но вам всё ещё нужна типизированная валидация без лишнего веса.
Что делать дальше
Проверьте один реальный путь в вашем приложении. Этого достаточно: регистрация, контактная форма или страница настроек. Свяжите форму с одним API-маршрутом, который работает с теми же данными, и посмотрите на три вещи: что попадает в клиентский бандл, насколько естественно выглядит общая схема и сколько работы нужно, чтобы превратить ошибки валидации во что-то полезное и для пользователей, и для разработчиков.
Сначала запускайте изменения узко. Не переносите все формы, все эндпоинты и все старые валидаторы за один проход. Один стабильный пример лучше, чем незавершённое переписывание по всему коду.
Также полезно оставить в репозитории короткую заметку о том, почему вы выбрали эту библиотеку, где она должна работать и как должны выглядеть ошибки. Через шесть месяцев такая заметка экономит много времени.
Если в кодовой базе уже смешаны разные подходы к валидации, а путь к переписыванию неясен, перед тем как шаблон распространится, может помочь второе мнение. Oleg Sotnikov на oleg.is занимается таким Fractional CTO advisory для команд, которым нужно балансировать лёгкий фронтенд, строгие API и практичные общие схемы.