Главная / Статьи / Общее использование одной схемы Zod между фронтендом на React и бэкендом на Node

Общее использование одной схемы Zod между фронтендом на React и бэкендом на Node

Узнайте, как одна схема Zod может проверять формы React, ответы API, тела запросов Express и переменные окружения, одновременно генерируя соответствующие типы TypeScript.

1536 слов

Проверка данных необходима в любом приложении, но команды часто добавляют её постепенно — отдельная библиотека для фронтенда, другая — для бэкенда, причем одни и те же правила копируются в нескольких местах. Zod стал любимым инструментом среди разработчиков JavaScript и TypeScript именно потому, что избавляет от такого беспорядка: вы пишете одну схему, которая одновременно проверяет ваши данные и генерирует соответствующий тип TypeScript, готовый к использованию как в браузере, так и на сервере.

1. Что такое Zod?

Zod — это библиотека схем, созданная с учётом TypeScript с самого начала. Вы описываете структуру данных один раз, и Zod использует это описание для проверки значений во время выполнения и для автоматического формирования типа TypeScript — нет необходимости писать отдельный интерфейс, и не существует риска дисбаланса между ним и правилами проверки.

import { z } from 'zod';
const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number }

Одна такая схема одновременно покрывает три функции: она документирует структуру данных, обеспечивает её соблюдение во время выполнения и предоставляет статический тип, от которого зависят редактор и компилятор.

2. Почему Zod превосходит альтернативы

Основное преимущество — автоматическое определение типов. Библиотеки вроде Yup или Joi обычно требуют от вас поддерживать схему проверки наряду с вручную написанным интерфейсом TypeScript, полагаясь на то, что они не будут расходиться по мере изменения кодовой базы. Zod полностью устраняет этот риск: тип выводится непосредственно из схемы, поэтому нет необходимости синхронизировать что-либо ещё.

Zod также имеет небольшой размер и не требует внешних зависимостей, что делает его одинаково удобным как в пакетах фронтенда, ориентированных на минимизацию размера, так и в сервисах Node.js. Его цепочечная, комбинируемая API позволяет даже при сложной валидации — с вложенными объектами, объединениями и полями, зависящими друг от друга — сохранять код понятным и читаемым, вместо того чтобы превращаться в клубок специальных вспомогательных функций.

3. Использование Zod в приложении React

3.1 Валидация форм с помощью React Hook Form

Zod подключается непосредственно к React Hook Form через пакет @hookform/resolvers.

npm install zod react-hook-form @hookform/resolvers
// components/SignupForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const SignupSchema = z.object({
  name: z.string().min(2, 'Name is too short'),
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});
type SignupData = z.infer<typeof SignupSchema>;
export function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignupData>({
    resolver: zodResolver(SignupSchema),
  });
  const onSubmit = (data: SignupData) => {
    console.log('Valid data:', data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Name" />
      {errors.name && <p>{errors.name.message}</p>}
      <input {...register('email')} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register('password')} placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Sign Up</button>
    </form>
  );
}

Не требуется ручной отслеживание состояния ошибок и нет необходимости в дублирующихся объявлениях типов — одна схема одновременно обрабатывает проверку валидности, предоставляет сообщения об ошибках рядом с каждым полем и определяет тип объекта data, отправляемого в TypeScript.

3.2 Проверка ответов API

Zod также полезен при обработке входных данных приложения — например, для проверки того, соответствуют ли данные, возвращаемые API, ожидаемым значениям, поскольку нельзя полагаться только на типы на этапе компиляции для гарантии этого.

import { z } from 'zod';
const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
});
const PostsResponseSchema = z.array(PostSchema);
async function fetchPosts() {
  const res = await fetch('/api/posts');
  const json = await res.json();
  const result = PostsResponseSchema.safeParse(json);
  if (!result.success) {
    console.error(result.error.flatten());
    throw new Error('Invalid API response shape');
  }
  return result.data; // fully typed Post[]
}

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

4. Использование Zod в фреймворке Node.js / Express

4.1 Проверка корпуса запросов

npm install zod express
// schemas/user-schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
// middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
export function validate(schema: ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.flatten() });
    }
    req.body = result.data;
    next();
  };
}
// routes/users.ts
import { Router } from 'express';
import { validate } from '../middleware/validate';
import { CreateUserSchema } from '../schemas/user-schema';
const router = Router();
router.post('/users', validate(CreateUserSchema), (req, res) => {
  // req.body is now guaranteed to match CreateUserInput
  const { name, email, age } = req.body;
  res.status(201).json({ name, email, age });
});
export default router;

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

4.2 Проверка переменных окружения

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

// config/env.ts
import { z } from 'zod';
const EnvSchema = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});
export const env = EnvSchema.parse(process.env);

Если отсутствует какая-либо необходимая переменная или она имеет неверный формат, процесс сразу же завершается с понятным сообщением об ошибке — это гораздо проще для диагностики, чем загадочная ошибка, скрытая внутри запроса к базе данных.

5. Настоящий преимущество: единая схема, используемая во всей стек-архитектуре

Поскольку схемы Zod представляют собой просто значения TypeScript, ничто не мешает разместить их в общем пакете или общей папке внутри монорепозитория и использовать абсолютно одинаковую схему как на клиенте, так и на сервере.

/packages
  /shared
    /schemas
      user-schema.ts   <-- used by both React app and Express API
  /web (React/Next.js)
  /api (Node/Express)
// packages/shared/schemas/user-schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

React-приложение использует эту схему для проверки формы регистрации перед её отправкой. API Express также опирается на ту же схему для верификации поступающих данных. Когда схема меняется — например, появляется новое обязательное поле — оба слоя синхронно реагируют на изменения, и TypeScript немедленно выявляет код, который ещё не адаптирован к новой структуре. Это предотвращает возникновение целого класса ошибок, когда проверки с клиентской и серверной сторон со временем начинают отклоняться друг от друга.

6. Лучшие практики

  • Используйте safeParse, когда неудача является нормальным и ожидаемым результатом (данные из формы, ответы от сторонних API), а parse, который вызывает исключение, оставьте для случаев, когда данные действительно никогда не могут быть недопустимыми, например, переменные окружения, проверяемые при запуске.
  • Храните общие схемы в одном пакете, когда у вас есть как фронтенд, так и бэкенд, чтобы не поддерживать две копии одних и тех же правил.
  • Используйте метод .transform() для очистки данных в процессе самой верификации — удаления пробелов, принудительной конвертации типов — вместо отдельной процедуры нормализации позже.
  • Вместо ручно написанных интерфейсов предпочитайте z.infer для всего, что уже имеет схему, чтобы ваши типы и логика верификации никогда не теряли синхронизации.
  • Возвращайте в ответах API ошибках значения error.flatten() или error.format(), что позволяет коду фронтенда легко соотносить каждую ошибку с соответствующим полем формы.
  • 7. Заключение

    Zod — это не просто типичная библиотека валидации: она полностью меняет подход к связи между валидацией и определением типов. Генерируя типы TypeScript непосредственно из схем во время выполнения, она без проблем устраняет проблему расхождений между определениями типов и правилами валидации. К этому добавляются её минимальные требования к ресурсам, модульная архитектура и одинаковое поведение как в браузере, так и в Node — всё это делает Zod идеальным выбором для полноценных проектов на TypeScript, построенных с использованием React и Node.js.

    Следующие шаги:

    • Изучите zod-to-openapi, если вам нужно генерировать документацию OpenAPI непосредственно из ваших схем
    • Ознакомьтесь с методами .refine() и .superRefine() для создания пользовательской логики валидации, охватывающей несколько полей
    • Посмотрите tRPC, который использует схемы Zod для обеспечения полной типобезопасности на всём протяжении API

    Связанные статьи

  • Тихие достижения TypeScript 6 и привычки веб-разработчиков высокого уровня в JavaScript — Узнайте о малоизвестных функциях TypeScript 6, таких как явное управление ресурсами и параметры типа const, а также об идиомах JavaScript, от которых ежедневно зависят ведущие инженеры.
  • req-guard-lite: Минималистичный лимитер запросов для Express, написанный на TypeScript — Ознакомьтесь с принципами работы легкого лимитера запросов для Express без внешних зависимостей, от стандартных режимов работы в памяти до масштабирования с использованием Redis и пользовательских генераторов ключей.