Startseite / Artikel / Ersetzen von as-Casts durch Zod-Parsing an jeder Next.js-Datengrenze

Ersetzen von as-Casts durch Zod-Parsing an jeder Next.js-Datengrenze

Warum ein TypeScript-Cast Sie nicht vor API-Drift schützen kann und wie eine Zod-Schema-Funktionsweise die Ergebnisse von fetch-Aufrufen, Formulare, Route-Handler sowie Server Actions in Next.js validiert.

1102 Wörter

Typisierte Komponenten wirken sicher, solange die Produktion kein umbenanntes Feld sendet, keinen null anstelle einer Zeichenkette oder keine Fehlermeldung anstelle eines Benutzers. TypeScript kann das nicht erkennen: Seine Typen verschwinden zur Kompilierzeit, während Netzwerkdaten nur zur Laufzeit vorhanden sind. Daher ist as User eine Behauptung und keine Überprüfung. Diese Anleitung zeigt, wie ein einzelnes Zod-Schema sowohl eingehende Daten validiert als auch den TypeScript-Typ erzeugt, sowie wie es an jeder Grenze einer React- und Next.js-Anwendung angewendet werden kann: bei fetch-Ergebnissen, Formularen, Route-Handlern und Server Actions.

Unzuverlässiges JSON ist das eigentliche Problem

Jeder Payload, den Ihr Code nicht selbst erstellt hat – sei es eine fetch-Antwort, ein Anfragekörper, Eingabedaten für eine Serveraktion oder ein webhook – verdient Misstrauen. Wenn Sie die Laufzeitprüfung überspringen, kommt es zu blinden Typumwandlungen, Validatoren, die von den Schnittstellen abweichen, sowie zu Inkonsistenzen zwischen Client- und Servertypen. Zod fasst all das in einer einzigen Definition zusammen: Ändern Sie das Schema, und der abgeleitete Typ ändert sich automatisch mit.

Definieren Sie das Schema, leiten Sie den Typ ab

Beginnen Sie mit dem Import:

import { z } from "zod";

Das untenstehende Schema beschreibt ein Benutzerprofil, z.infer wandelt es in einen TypeScript-Typ um, und loadProfile führt die Antwort vor dem Zurückgeben durch parse.

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

Vergleichen Sie return data as UserProfile: Die Parsing-Logik wirft Fehler, sobald die API das vereinbarte Format bricht, während die Typumwandlung es zulässt, dass fehlerhafte Daten weitergeleitet werden, bis schließlich weit entfernt vom ursprünglichen Problem ein Fehler auftritt.

In UI-Code ist safeParse in der Regel besser: Es gibt ein Ergebnisobjekt zurück anstelle eines Fehlers, sodass Sie selbst über die fallback-Maßnahmen entscheiden können:

const result = UserProfileSchema.safeParse(data);
if (!result.success) {
  console.error(result.error.flatten());
  return null;
}

Formulare, die einen gültigen Datensatz an den Submit-Handler übergeben

Mit zodResolver prüft React Hook Form die Werte, bevor sie handleSubmit erreichen. Die Datei ist ein Client-Komponente:

"use client";

Auch die Feldfehlermeldungen stammen aus dem Schema, wodurch die Benutzeroberflächenanzeige und die Datentypen im Einklang bleiben:

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

In einer echten Anwendung sollte SignupSchema in ein gemeinsames Modul verschoben werden, anstatt es innerhalb der Komponentendatei zu definieren, damit der Server dieselben Regeln importieren kann.

Validierung an den Next.js-Eintrittspunkten

Routenhandler

Ein Routenhandler benötigt NextResponse, Zod sowie das gemeinsame Profilschema:

import { NextResponse } from "next/server";
import { z } from "zod";
import { UserProfileSchema } from "@/lib/schemas/user";

Der Handler validiert den Inhalt mit safeParse und gibt bei Misserfolg einen Status 400 zusammen mit den detaillierten Fehlern zurück. Zudem wird die eigene Antwort anhand von UserProfileSchema analysiert, sodass das Ausgabeformat den Anforderungen des Clients entspricht. Der fest codierte id steht für eine Eingabe in die Datenbank.

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

Server-Aktionen

Ein Server-Action-Modul beginnt mit der Anweisung:

"use server";

Die Aktion erstellt ein Objekt aus FormData und validiert es gegen dasselbe SignupSchema, wie es auch im Formular verwendet wird. Durch die Rückgabe von ok als Literaltyp (as const) können die Aufrufer das Ergebnis sauber filtern:

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

Ein Schema-Modul, das von Client und Server gemeinsam verwendet wird, behebt den Fehler „gültig im Formular, vom Server abgelehnt“. Für das gleiche Szenario außerhalb von Next.js siehe das Teilen eines Zod-Schemas zwischen einer React-Frontend und einem Node-Backend.

Gewohnheiten, die die Wartbarkeit von Schemata gewährleisten

  • Halten Sie die Schemata zusammen, zum Beispiel unter lib/schemas/*.
  • Erstellen Sie Varianten mithilfe von .extend, .pick und .omit anstelle von Feldduplikaten.
  • Verwenden Sie .transform nur für kleine Aufbereitungen wie das Trimmen von Zeichenketten oder das Parsen von Datumsangaben, niemals für versteckte Geschäftsregeln.
  • Verwenden Sie z.discriminatedUnion, wenn die Struktur eines Payloads von einem Statusfeld abhängt.
  • Parsen Sie Umgebungsvariablen einmal, beim Starten des Programms.

In der Praxis sieht die Komposition so aus. Ein Basis-Schema enthält gemeinsame Felder:

const BaseUser = z.object({
  email: z.string().email(),
  displayName: z.string().optional(),
});

Daraus erstellt ein Aktualisierungs-Schema mit .partial() jedes Feld zu optional und ein Response-DTO fügt mit .extend() serverseitige Felder hinzu:

export const UpdateUserSchema = BaseUser.partial();
export const UserDtoSchema = BaseUser.extend({
  id: z.string().uuid(),
  createdAt: z.string().datetime(),
});

Eine Einschränkung: Neuere Zod-Versionen haben oberste Ebene-Formate wie z.email() und z.uuid() eingeführt sowie die Art und Weise, wie Fehler abgehandelt werden, geändert. Die hier gezeigten verschachtelten Formate könnten in Ihrer Version veraltet sein, daher sollten Sie die aktuellen Zod-Dokumentationen prüfen.

Wichtige Erkenntnisse

  • Typen beschreiben die Absicht; nur die Laufzeit-Parsing-Funktionen stellen diese am Netzwerkrand durch.
  • Erfassen Sie TypeScript-Typen aus Zod-Schemata, damit sich die beiden nicht voneinander entfernen.
  • Verwenden Sie safeParse, wenn Sie mit Fehlern umgehen möchten, und parse, wenn Fehler ausgelöst werden sollen.
  • Verwenden Sie ein und dasselbe Schema für das Formular, den Handler und die Aktion.
  • Wählen Sie den gefährlichsten as-Cast aus und geben Sie ihm zunächst ein Schema.