Strona główna / Artykuły

Dzielenie się jednym schematem Zod pomiędzy frontendem w React a backendem w Node

Dowiedz się, jak jeden schemat Zod może weryfikować formularze React, odpowiedzi API, ciała żądań Express oraz zmienne środowiskowe, jednocześnie generując odpowiadające im typy TypeScript.

7 min czytania1709 słów

Walidacja powinna być obecna we wszystkich aplikacjach, ale zespoły często dodają ją stopniowo — jedna biblioteka do frontendu, inna do backendu, a te same reguły kopiowane w kilku miejscach. Zod stał się ulubionym narzędziem deweloperów JavaScript i TypeScript właśnie dlatego, że unika tego bałaganu: piszesz jeden schemat, który zarówno sprawdza twoje dane, jak i generuje odpowiadający mu typ TypeScript, gotowy do użycia identycznie w przeglądarce i na serwerze.

1. Czym jest Zod?

Zod to biblioteka do tworzenia schematów stworzona od samego początku z myślą o TypeScript. Opisujesz kształt swoich danych raz, a Zod wykorzystuje ten opis do sprawdzania wartości w czasie wykonywania oraz do automatycznego generowania typu TypeScript — nie ma potrzeby pisania oddzielnej interfejsu, a także nie istnieje ryzyko rozbieżności z twoimi regułami walidacji.

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 }

Jedno taki schemat obejmuje jednocześnie trzy role: dokumentuje strukturę danych, egzekwuje ją w czasie wykonywania oraz dostarcza statycznego typu, od którego zależą edytor i kompilator.

2. Dlaczego Zod przewyższa inne rozwiązania

Najważniejszą zaletą jest automatyczna inferencja typów. Biblioteki takie jak Yup lub Joi zazwyczaj wymagają utrzymywania schematu walidacji obok ręcznie napisanej interfejsu w TypeScript, polegając na tym, że oba nie będą się różnić w miarę zmian w kodzie. Zod całkowicie eliminuje to ryzyko: typ jest wywodzony bezpośrednio ze schematu, więc nie ma potrzeby synchronizowania żadnych elementów.

Zod jest również lekki i nie wymaga żadnych zewnętrznych zależności, co sprawia, że równie dobrze sprawdza się w pakietach frontendu dbających o rozmiar plików, jak i w usługach Node.js. Jego łańcuchowa, kompozytowa API oznacza również, że nawet złożone procedury walidacji — obiekty nawarstwione, unie, pola wzajemnie się od siebie zależne — pozostają jasne i łatwe do odczytania, zamiast przeradzać się w plątaninę ad hoc funkcji pomocniczych.

3. Użycie Zod w aplikacji React

3.1 Walidacja formularzy z użyciem React Hook Form

Zod łączy się bezpośrednio z React Hook Form za pośrednictwem pakietu @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>
  );
}

Nie ma ręcznego śledzenia stanu błędów ani konieczności utrzymywania podwójnych deklaracji typów – jedno schemat zarządza walidacją, dostarcza komunikaty o błędach pokazywane obok każdego pola oraz jednocześnie definiuje typ TypeScript dla przesłanego obiektu data.

3.2 Walidacja odpowiedzi API

Zod jest równie przydatny po stronie odbierającej dane w aplikacji – na przykład podczas sprawdzania, czy dane zwrócone przez API rzeczywiście odpowiadają temu, czego oczekujemy, ponieważ nie można polegać wyłącznie na typach z czasu kompilacji, aby to zagwarantować.

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[]
}

Taki podejście umożliwia wykrycie niepoprawnych lub nieoczekiwanych odpowiedzi, zanim spowodują one ciche błędy w interfejsie użytkownika.

4. Używanie Zod w backendzie Node.js / Express

4.1 Walidacja ciał żądań

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;

Taka konfiguracja zapewnia każdej trasie jednolity, deklaratywny krok walidacji, przy czym obsługa błędów jest centralizowana zamiast być powtarzana jako wewnętrzne sprawdzenia if w poszczególnych obsługiwaczach.

4.2 Walidacja zmiennych środowiskowych

Jednym z niedocenianych, ale skutecznych zastosowań Zod jest sprawdzanie wartości process.env przy uruchamianiu aplikacji, dzięki czemu błędna konfiguracja powoduje natychmiastową awarię zamiast późniejszego, mylącego błędu.

// 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);

Jeśli brakuje jakiejś wymaganej zmiennej lub ma ona niewłaściwy format, proces natychmiast się zatrzymuje z czytelnym komunikatem o błędzie — co znacznie ułatwia diagnozę w porównaniu z tajemniczą awarią wynikającą z wywołania bazy danych.

5. Prawdziwy atut: jedna struktura danych, współdzielona w całym stacku

Ponieważ struktury Zod to po prostu wartości typu TypeScript, nic nie stoi na przeszkodzie, by umieścić je w wspólnym pakiecie — lub we wspólnej folderze w monorepo — i ponownie używać **identycznej struktury danych** zarówno na stronie klienta, jak i serwerze.

/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>;

Aplikacja React polega na tym schemacie do sprawdzania formularza rejestracyjnego przed jego wysłaniem. API Express również wykorzystuje ten sam schemat do walidacji otrzymywanych danych. Gdy schemat ulega zmianie – na przykład pojawia się nowe pole obowiązkowe – oba warstwy reagują na tę zmianę jednocześnie, a TypeScript natychmiast wskazuje na kod, który jeszcze nie został dostosowany do nowej struktury. Dzięki temu eliminuje się całą klasę błędów, w których walidacja po stronie klienta i serwera stopniowo się od siebie oddalają z upływem czasu.

6. Najlepsze praktyki

  • Używaj safeParse, gdy niepowodzenie jest normalnym, oczekiwanym wynikiem (dane z formularza, odpowiedzi API od dostawców zewnętrznych), a parse – który wywołuje błąd – zachowaj dla przypadków, które rzeczywiście nigdy nie powinny być nieważne, takich jak zmienne środowiskowe sprawdzane przy uruchamianiu aplikacji.
  • Zawsze przechowuj wspólne schematy w jednym pakiecie, gdy zarządzasz zarówno frontendem, jak i backendem, aby uniknąć utrzymywania dwóch kopii tych samych reguł.
  • Wykorzystaj metodę .transform() do oczyszczania danych w ramach samej walidacji — usuwania spacji, konwersji typów — zamiast wykonywać oddzielną operację normalizacji później.
  • Wolij metodę z.infer zamiast ręcznie napisanych interfejsów dla wszystkiego, co jest już obsługiwane przez schemat, dzięki czemu twoje typy i logika walidacji nigdy nie będą niesynchronizowane.
  • W odpowiedziach błędów API wysyłaj error.flatten() lub error.format(), co umożliwia kodowi frontendu łatwe powiązanie każdego błędu z odpowiednim polem formularza.
  • 7. Podsumowanie

    Zod to coś więcej niż zwykła biblioteka do walidacji — całkowicie zmienia relację pomiędzy walidacją a typowaniem. Dzięki generowaniu typów TypeScript bezpośrednio z schematów w czasie wykonywania, eliminuje problem rozbieżności pomiędzy definicjami typów a regułami walidacji. Do tego dochodzi jego niewielki rozmiar, projekt typu kompozytowego oraz spójne zachowanie zarówno w przeglądarce, jak i w Node, co sprawia, że Zod jest doskonałym wyborem dla projektów full-stack w TypeScript budowanych na React i Node.js.

    Kolejne kroki:

    • Rozważ użycie zod-to-openapi, jeśli potrzebujesz generować dokumentację OpenAPI bezpośrednio z swoich schematów
    • Przyjrzyj się funkcjom .refine() i .superRefine(), aby stworzyć niestandardową logikę walidacji obejmującą kilka pól
    • Zapoznaj się z tRPC, który wykorzystuje schematy Zod w sposób natywny, zapewniając pełną bezpieczeństwo typów w całej API