Zastępowanie as-Casts przez Zod Parsing przy każdej granicy danych w Next.js
Dlaczego przekształcenie typu w TypeScript nie chroni przed zmianami API, oraz jak schemat Zod waliduje wyniki fetch, formy, obsługę tras i działania serwera w Next.js.
Komponenty napisane w tym języku wydają się bezpieczne, dopóki produkcja nie prześle pola o zmienionej nazwie, wartości null zamiast ciągu znaków lub błędu zamiast użytkownika. TypeScript nie może tego wykryć – jego typy znikają w czasie kompilacji, podczas gdy dane sieciowe istnieją tylko w czasie wykonywania, więc as User jest jedynie stwierdzeniem, a nie sprawdzeniem. Ten przewodnik pokazuje, jak pojedyncze schemat Zod może zarówno weryfikować przychodzące dane, jak i tworzyć odpowiedni typ w TypeScriptie, oraz jak zastosować je na każdym etapie aplikacji React i Next.js: przy wynikach fetch, formularzach, obsługujących trasę oraz działaniach serwera.
Prawdziwym problemem jest niezaufane JSON
Każdy ciężar danych, którego twój kod nie stworzył samodzielnie – czy to odpowiedź z fetch, ciało żądania, dane wejściowe do Server Action czy webhook – zasługuje na podejrzenie. Jeśli pominiesz weryfikację w czasie wykonywania, pojawią się niekontrolowane przekształcenia typów, walidatory, które odbiegają od określonych interfejsów, oraz typy klienta i serwera, które się ze sobą nie zgadzają. Zod łączy to wszystko w jedną definicję: wystarczy edytować schemat, a typ zostanie automatycznie zaktualizowany.
Zdefiniuj schemat, wywnioskuj typ
Zacznij od importu:
import { z } from "zod";
Poniższy schemat opisuje profil użytkownika, z.infer przekształca go w typ TypeScript, a loadProfile przetwarza odpowiedź za pomocą parse przed jej zwróceniem.
export const UserProfileSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
displayName: z.string().min(1).optional(),
});export type UserProfile = z.infer<typeof UserProfileSchema>;async function loadProfile(id: string): Promise<UserProfile> {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return UserProfileSchema.parse(data);
}
Porównaj return data as UserProfile: parsowanie powoduje błąd, gdy tylko API narusza umowę, natomiast przekształcenie typu pozwala na przesyłanie nieprawidłowych danych, aż do momentu awarii daleko od źródła problemu.
W kodzie interfejsu użytkownika zazwyczaj lepszym rozwiązaniem jest safeParse: zwraca on obiekt z wynikiem zamiast rzucać błędem, dzięki czemu sam decydujesz o rozwiązaniu awaryjnym:
const result = UserProfileSchema.safeParse(data);
if (!result.success) {
console.error(result.error.flatten());
return null;
}
Formularze, które przekazują do funkcji obsługi wysyłki poprawne dane
Za pomocą zodResolver React Hook Form weryfikuje wartości przed tym, jak dotrą one do handleSubmit. Ten plik stanowi komponent kliencki:
"use client";
Wiadomości o błędach w polach pochodzą również ze schematu, co zapewnia zgodność informacji zwrotnych w interfejsie z typami danych:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";const SignupSchema = z.object({
email: z.string().email("Enter a valid email"),
password: z.string().min(8, "At least 8 characters"),
});type SignupValues = z.infer<typeof SignupSchema>;export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupValues>({
resolver: zodResolver(SignupSchema),
}); return (
<form onSubmit={handleSubmit((values) => console.log(values))}>
<input type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Create account</button>
</form>
);
}
W rzeczywistym aplikacji warto przenieść SignupSchema do wspólnego modułu zamiast definiować go wewnątrz pliku komponentu, aby serwer mógł importować identyczne zasady.
Weryfikacja w punktach wejścia Next.js
Obsługa tras
Obsługa trasy wymaga obiektu NextResponse, biblioteki Zod oraz wspólnego schematu profilu:
import { NextResponse } from "next/server";
import { z } from "zod";
import { UserProfileSchema } from "@/lib/schemas/user";
Obsługa waliduje treść za pomocą safeParse i w przypadku niepowodzenia zwraca kod 400 wraz ze spłaszczonymi błędami. Ponadto analizuje własną odpowiedź pod kątem UserProfileSchema, dzięki czemu wynik jest zgodny z umową klienta. Łącznie skonfigurowany id zastępuje operację wprowadzania danych do bazy.
const CreateUserSchema = z.object({
email: z.string().email(),
displayName: z.string().min(1).max(80).optional(),
});export async function POST(request: Request) {
const parsed = CreateUserSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid body", details: parsed.error.flatten() },
{ status: 400 }
);
} const created = {
id: "11111111-1111-1111-1111-111111111111",
email: parsed.data.email,
displayName: parsed.data.displayName,
}; return NextResponse.json(UserProfileSchema.parse(created), { status: 201 });
}
Działania serwera
Moduł Działania Serwera zaczyna się od dyrektywy:
"use server";
Działanie tworzy obiekt na podstawie FormData i waliduje go według tego samego SignupSchema, który był używany w formularzu. Zwracanie wartości ok jako typu literalnego (as const) umożliwia wywołującym czyste zawężenie wyniku:
import { SignupSchema } from "@/lib/schemas/auth";export async function signupAction(formData: FormData) {
const parsed = SignupSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
}); if (!parsed.success) {
return { ok: false as const, errors: parsed.error.flatten().fieldErrors };
} return { ok: true as const };
}
Jeden moduł schematu udostępniany zarówno przez klienta, jak i serwer eliminuje problem „danych ważnych w formularzu, odrzuconych przez serwer”. Aby zapoznać się z podobnym podejściem poza Next.js, sprawdź udostępnianie jednego schematu Zod pomiędzy frontendem React a backendem Node.
Nawyki zapewniające łatwą konserwację schematów
- Zachowuj schematy razem, na przykład w katalogu
lib/schemas/*. - Twórz warianty za pomocą metod
.extend,.picki.omitzamiast kopiować pola. - Wykorzystuj metodę
.transformdo prostych operacji takich jak usuwanie spacji z ciągów znaków lub parsowanie dat, a nie do realizacji ukrytych zasad biznesowych. - Używaj funkcji
z.discriminatedUnion, gdy struktura danych zależy od pola statusu. - Parsuj zmienne środowiskowe tylko raz, podczas uruchamiania aplikacji.
W praktyce kompozycja wygląda w ten sposób. Podstawowe schemat zawiera wspólne pola:
const BaseUser = z.object({
email: z.string().email(),
displayName: z.string().optional(),
});
Na jego podstawie schemat aktualizacji czyni każde pole opcjonalnym za pomocą .partial(), a DTO odpowiedzi dodaje pola należące do serwera za pomocą .extend():
export const UpdateUserSchema = BaseUser.partial();
export const UserDtoSchema = BaseUser.extend({
id: z.string().uuid(),
createdAt: z.string().datetime(),
});
Jedna uwaga: nowsze wersje Zod wprowadziły formaty najwyższego poziomu, takie jak z.email() i z.uuid(), oraz zmieniły sposób prezentacji spłaszczania błędów. Pokazane tutaj łańcuchowe formy mogą być przestarzałe w twojej wersji, więc sprawdź aktualne dokumentacje Zod.
Główne wnioski
- Typy opisują intencję; tylko analiza w czasie wykonywania egzekwuje ją na granicy sieci.
- Wyprowadzaj typy TypeScript z schematów Zod, aby oba nie rozchodziły się od siebie.
- Wybieraj
safeParse, gdy chcesz obsłużyć błędy, aparse, gdy błąd powinien być rzucony.
as i najpierw przypisz jej schemat.Literatura pokrewna
- Formularz daty urodzenia weryfikowany w Next.js z kontrolowanymi polami wejściowymi i callbackami — Stwórz mały formularz kliencki w Next.js, który śledzi dane wejściowe za pomocą useState, odrzuca niewłaściwe daty urodzenia, wyświetla czytelne błędy i przekazuje czyste dane do komponentu nadrzędnego.
- Oddzielanie warstw domeny, danych i interfejsu w kodzie Next.js App Router — Studium przypadku Pokédex pokazujące, jak podzielić aplikację Next.js App Router na warstwy domeny, danych i prezentacji przy użyciu Prisma, Zod, autoryzacji cookie oraz cache’owania.
- Zod 4.5: kompilacja, walidacja i safeParse – wybieranie parsera dla każdej granicy — Jak przeprowadzić testy wydajności skompilowanych parserów Zod 4.5 oraz szybkiej ścieżki walidacji logicznej na poprawnych i niepoprawnych danych, oraz gdzie powinny znajdować się poszczególne interfejsy API bez utraty informacji o błędach.
- Branded IDs w TypeScript: które kodowania naprawdę zapobiegają błędnemu usunięciu — Sześć sposobów definiowania typów UserId i InvoiceId w porównaniu w ramach jednego testu: które z nich sprawiają, że tsc odrzuca wywołanie deleteInvoice(userId), oraz w jaki sposób Zod zapewnia bezpieczeństwo w czasie wykonywania.