Strona główna / Artykuły / Dzielenie się jednym schematem Zod pomiędzy frontendem w React a backendem w Node

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.

1536 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

    Powiązane artykuły

  • Ciche zwycięstwa TypeScript 6 i nawyki JavaScript u doświadczonych developerów — Poznaj pomijane funkcje TypeScript 6, takie jak wyraźne zarządzanie zasobami i parametry typu const, a także idiomaty JavaScript, na których codziennie polegają doświadczeni inżynierowie.
  • req-guard-lite: Minimalny rate limiter w TypeScript dla Express — Dowiedz się, jak działa lekki rate limiter dla Express bez żadnych zależności, od domyślnych rozwiązań w pamięci operacyjnej po skalowanie za pomocą Redis i niestandardowe generatory kluczy.