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.
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), aparse– 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.
.transform() do oczyszczania danych w ramach samej walidacji — usuwania spacji, konwersji typów — zamiast wykonywać oddzielną operację normalizacji później.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.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
- Zod vs express-validator: Dwie metody walidacji w Express — Porównuje walidację żądań opartą na schematach z Zod z interfejsem middleware express-validator bazującym na łańcuchach, omawiając konfigurację, formatowanie błędów oraz częste pułapki.
- Propozycje TC39 z 2026 roku: Dekoratory, Temporal i Signals wyjaśnione — Praktyczny przegląd trzech propozycji TC39 – wbudowanych dekoratorów, API Temporal oraz Signals – i tego, co one oznaczają dla programistów JavaScript i TypeScript pracujących w pełnym stacku.