Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Walidacja formularzy z Zod

W Metropolis Quantum każdy system wymaga precyzyjnej walidacji danych — od terminali dostępowych po panele kontroli implantów. Zod to biblioteka walidacyjna zaprojektowana z myślą o TypeScript, która stała się standardem w ekosystemie Next.js. W przeciwieństwie do innych rozwiązań, Zod automatycznie generuje typy TypeScript na podstawie schematów walidacji, eliminując duplikację kodu.

Dlaczego Zod?

W roku 2150, systemy Metropolis Quantum wymagają niezawodnej walidacji danych. Zod wyróżnia się na tle innych bibliotek walidacyjnych (Yup, Joi) z kilku powodów:

  1. TypeScript-first — schematy Zod automatycznie generują typy, eliminując duplikację
  2. Zero zależności — lekka biblioteka (~10 KB gzipped)
  3. Standard Next.js — oficjalnie rekomendowany w dokumentacji Next.js
  4. Działa na serwerze i kliencie — idealne dla Server Actions

Instalacja i import

1npm install zod
1import { z } from 'zod';

Podstawowe schematy Zod

Zod oferuje zestaw metod do definiowania typów danych — jak cyfrowe DNA opisujące strukturę informacji w systemie:

1import { z } from 'zod';
2
3// Typy prymitywne
4const nameSchema = z.string();
5const ageSchema = z.number();
6const isActiveSchema = z.boolean();
7
8// Schema obiektu — łączymy typy w strukturę
9const userSchema = z.object({
10  name: z.string(),
11  email: z.string().email(),
12  age: z.number().int().positive(),
13  isActive: z.boolean(),
14});

Każdy

z.string()
,
z.number()
,
z.boolean()
to podstawowy blok budulcowy schematu. Metoda
z.object()
łączy je w kompleksową strukturę — jak schemat terminala w Metropolis Quantum.

Metody walidacji: parse i safeParse

Zod oferuje dwa sposoby walidacji danych:

1import { z } from 'zod';
2
3const schema = z.object({
4  name: z.string().min(2),
5  email: z.string().email(),
6});
7
8// 1. parse() — rzuca wyjątek przy błędnych danych
9try {
10  const data = schema.parse({ name: "Neo", email: "neo@quantum.io" });
11  console.log("Dane poprawne:", data);
12} catch (error) {
13  console.error("Błąd walidacji:", error);
14}
15
16// 2. safeParse() — NIE rzuca wyjątku, zwraca obiekt z wynikiem
17const result = schema.safeParse({ name: "", email: "zly-email" });
18
19if (result.success) {
20  console.log("Dane:", result.data);
21} else {
22  console.log("Błędy:", result.error.errors);
23  // [{ path: ["name"], message: "String must contain at least 2 character(s)" }, ...]
24}

Zasada: Używaj

safeParse()
w formularzach i Server Actions (bezpieczniejsze), a
parse()
gdy chcesz szybko przerwać wykonanie przy błędnych danych.

Komunikaty błędów i reguły walidacji

Zod pozwala na precyzyjne definiowanie reguł walidacji z własnymi komunikatami — jak konfiguracja systemu bezpieczeństwa w terminalu:

1import { z } from 'zod';
2
3const registrationSchema = z.object({
4  username: z.string()
5    .min(3, { message: "Nazwa użytkownika musi mieć co najmniej 3 znaki" })
6    .max(20, { message: "Nazwa użytkownika może mieć maksymalnie 20 znaków" }),
7  email: z.string()
8    .email({ message: "Nieprawidłowy format adresu email" }),
9  age: z.number()
10    .int({ message: "Wiek musi być liczbą całkowitą" })
11    .min(13, { message: "Musisz mieć co najmniej 13 lat" }),
12  password: z.string()
13    .min(8, { message: "Hasło musi mieć co najmniej 8 znaków" })
14    .regex(/[A-Z]/, { message: "Hasło musi zawierać wielką literę" })
15    .regex(/[0-9]/, { message: "Hasło musi zawierać cyfrę" }),
16  acceptTerms: z.boolean()
17    .refine(val => val === true, {
18      message: "Musisz zaakceptować warunki korzystania z systemu"
19    }),
20});

Metoda

.refine()
pozwala na definiowanie własnych, niestandardowych reguł walidacji — przydatne, gdy wbudowane metody nie wystarczają.

Inferowanie typów: z.infer

Jedną z najpotężniejszych cech Zod jest automatyczne generowanie typów TypeScript ze schematów. Nie musisz utrzymywać oddzielnych interfejsów:

1import { z } from 'zod';
2
3const implantSchema = z.object({
4  id: z.string().uuid(),
5  name: z.string().min(1),
6  powerLevel: z.number().min(0).max(100),
7  isActive: z.boolean(),
8  tags: z.array(z.string()),
9});
10
11// Typ generowany automatycznie ze schematu
12type Implant = z.infer<typeof implantSchema>;
13// Odpowiednik ręcznego: { id: string; name: string; powerLevel: number; isActive: boolean; tags: string[] }
14
15// Teraz typ i walidacja są ZSYNCHRONIZOWANE
16function processImplant(data: Implant) {
17  console.log(data.name); // TypeScript zna typ!
18}

Dzięki

z.infer
masz jedno źródło prawdy — schemat definiuje zarówno walidację, jak i typ TypeScript.

Integracja z React Hook Form

Zod doskonale współpracuje z React Hook Form dzięki adapterowi

zodResolver
. To najczęściej spotykany wzorzec w aplikacjach Next.js:

1import { useForm } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4
5// 1. Definiujemy schemat
6const loginSchema = z.object({
7  email: z.string()
8    .email({ message: "Nieprawidłowy adres email" }),
9  password: z.string()
10    .min(8, { message: "Hasło musi mieć co najmniej 8 znaków" }),
11});
12
13// 2. Generujemy typ
14type LoginForm = z.infer<typeof loginSchema>;
15
16// 3. Używamy w komponencie
17export default function LoginPage() {
18  const {
19    register,
20    handleSubmit,
21    formState: { errors, isSubmitting }
22  } = useForm<LoginForm>({
23    resolver: zodResolver(loginSchema), // Zod jako walidator
24    defaultValues: { email: '', password: '' }
25  });
26
27  const onSubmit = async (data: LoginForm) => {
28    // data jest już zwalidowane i typowane!
29    console.log("Logowanie:", data);
30  };
31
32  return (
33    <form onSubmit={handleSubmit(onSubmit)}>
34      <input {...register('email')} placeholder="Email" />
35      {errors.email && <p>{errors.email.message}</p>}
36
37      <input {...register('password')} type="password" placeholder="Hasło" />
38      {errors.password && <p>{errors.password.message}</p>}
39
40      <button type="submit" disabled={isSubmitting}>
41        {isSubmitting ? 'Logowanie...' : 'Zaloguj się'}
42      </button>
43    </form>
44  );
45}

Kluczowa linia to

resolver: zodResolver(loginSchema)
— łączy schemat Zod z systemem walidacji React Hook Form.

Reusable schematy i kompozycja

W dużych aplikacjach warto tworzyć reusable schematy, które można łączyć i rozszerzać:

1import { z } from 'zod';
2
3// Bazowy schemat adresu — używany w wielu miejscach
4const addressSchema = z.object({
5  street: z.string().min(1, "Ulica jest wymagana"),
6  city: z.string().min(1, "Miasto jest wymagane"),
7  postCode: z.string().regex(/^d{2}-d{3}$/, "Format: XX-XXX"),
8});
9
10// Schemat użytkownika — rozszerzamy o adres
11const userSchema = z.object({
12  name: z.string().min(2),
13  email: z.string().email(),
14  address: addressSchema,                    // Zagnieżdżanie
15  shippingAddress: addressSchema.optional(),  // Opcjonalny
16});
17
18// .merge() — łączenie dwóch schematów
19const baseProfile = z.object({ name: z.string(), bio: z.string() });
20const socialProfile = z.object({ twitter: z.string(), github: z.string() });
21const fullProfile = baseProfile.merge(socialProfile);
22
23// .extend() — dodawanie pól do istniejącego schematu
24const adminUser = userSchema.extend({
25  role: z.literal('admin'),
26  permissions: z.array(z.string()),
27});
28
29type AdminUser = z.infer<typeof adminUser>;

Zaawansowane techniki: refine i superRefine

Metoda

.refine()
pozwala na walidację, której nie da się wyrazić deklaratywnie — np. porównanie dwóch pól:

1import { z } from 'zod';
2
3const passwordSchema = z.object({
4  password: z.string().min(8),
5  confirmPassword: z.string(),
6}).refine(data => data.password === data.confirmPassword, {
7  message: "Hasła nie są identyczne",
8  path: ["confirmPassword"], // Błąd przypisany do pola confirmPassword
9});
10
11// superRefine — gdy potrzebujesz wielu błędów jednocześnie
12const advancedSchema = z.object({
13  accountType: z.enum(['personal', 'business']),
14  personalId: z.string().optional(),
15  companyId: z.string().optional(),
16}).superRefine((data, ctx) => {
17  if (data.accountType === 'personal' && !data.personalId) {
18    ctx.addIssue({
19      code: z.ZodIssueCode.custom,
20      message: "ID osobiste jest wymagane dla konta osobistego",
21      path: ['personalId'],
22    });
23  }
24  if (data.accountType === 'business' && !data.companyId) {
25    ctx.addIssue({
26      code: z.ZodIssueCode.custom,
27      message: "ID firmy jest wymagane dla konta biznesowego",
28      path: ['companyId'],
29    });
30  }
31});

Alternatywy: Yup i Joi

Zod to standard w ekosystemie Next.js, ale istnieją alternatywy:

  • Yup — popularna z Formik, deklaratywna składnia, ale wymaga ręcznych definicji typów TypeScript
  • Joi — potężna, bogata w funkcje, ale duża (~40 KB) i głównie serwerowa

Zod jest rekomendowany dla projektów Next.js, ponieważ łączy walidację z typami TypeScript i działa zarówno na serwerze, jak i kliencie.

Podsumowanie

Zod to niezbędne narzędzie w arsenale programisty Next.js:

  • z.string()
    ,
    z.number()
    ,
    z.object()
    — definiowanie schematów
  • parse()
    i
    safeParse()
    — walidacja danych
  • .min()
    ,
    .email()
    ,
    .regex()
    — reguły z komunikatami błędów
  • .refine()
    — niestandardowa walidacja
  • zodResolver
    — integracja z React Hook Form
  • z.infer<typeof schema>
    — automatyczne generowanie typów
Ir a CodeWorlds