Zurück zur Startseite

Formulare in Next.js als Vertrag: Zod und einheitliche Regeln

Der Artikel erklärt, wie man Formulare in Next.js nicht als UI-Komponenten, sondern als Verträge zwischen Oberfläche, Validierung und Server behandelt. Mit Zod, safeParse und flatten lassen sich Regeln, Fehlerformat und Zustände vereinheitlichen, was eine vorhersehbare UX und keine Diskrepanzen zwischen Client und Server gewährleistet.

So verwandeln Sie Formulare in Next.js mit Zod in einen zuverlässigen Vertrag
Advertisement 728x90

Formulare in Next.js als Vertrag: Zod, fieldErrors und einheitliche Regeln auf Client und Server

Ein Formular ist nicht nur eine Sammlung von Feldern und Buttons – es ist ein Vertrag zwischen UI, Validierung und Datenquelle. Wenn dieser Vertrag nicht explizit definiert ist, bricht Chaos aus: Die UI hält die Daten für gültig, der Server wirft einen Fehler, Nachrichten tauchen zufällig auf, und das Verhalten des Formulars ist unvorhersehbar. Die Lösung? Vereinheitliche das Validierungsschema, das Fehlerformat und die Zustände mit Zod, safeParse und flatten, um identische Regeln sowohl auf dem Client als auch auf dem Server zu gewährleisten.

Warum Formulare ohne Vertrag auseinanderfallen

Viele Projekte starten einfach: Input, State, Submit. Das funktioniert prima für kleine Formulare. Aber wenn der Codebase wächst, tauchen drei kritische Abweichungspunkte auf:

  • Inkonsistente Validierungsregeln. Der Client prüft die String-Länge, der Server aber auch verbotene Wörter oder Leerzeichen. Nutzer sehen ein grünes Häkchen, doch der Submit schlägt fehl.
  • Nicht passende Fehlerformate. Mal ein String, dann ein Objekt, danach ein Array. Die UI muss die Struktur der Response erraten.
  • Nicht definierte Zustände. Pending, disabled, success, formError – jedes Component geht anders damit um, was zu einer zersplitterten UX führt.

Ohne einen einzigen Vertrag folgt jedes Formular seinen eigenen lokalen Regeln. Das skaliert nicht.

Google AdInline article slot

Das Formular als Vertrag: Wichtige Elemente

Ein Formularvertrag muss diese Fragen eindeutig beantworten:

  • Welche Daten gelten als gültig?
  • Welche Fehler betreffen spezifische Felder (fieldErrors), und welche die Gesamtoperation (formError)?
  • Wie wird der Pending-Zustand gehandhabt?
  • Was wird bei Erfolg zurückgegeben?
  • Kann der Server eine Antwort in einem anderen Format liefern?
  • Werden dieselben Regeln auf Client und Server angewendet?

Diese Antworten sollten feststehen, bevor die UI implementiert wird. In Workbench hat dieser Ansatz Formulare um Zod-Schemas und eine einheitliche Response-Struktur standardisiert.

Ein Zod-Schema, zwei Einsatzmöglichkeiten

Statt die Validierungslogik zwischen Client und Server zu duplizieren, nutzt du überall ein einziges Zod-Schema. Beispielschema für eine Notiz:

Google AdInline article slot
import { z } from "zod";

export const noteSchema = z.object({
  title: z
    .string()
    .trim()
    .min(3, "Enter minimum 3 simvola")
    .max(80, "Maximum 80 characters"),
  description: z
    .string()
    .trim()
    .max(500, "Maximum 500 characters")
    .optional()
    .or(z.literal("")),
}).refine(
  data => data.title.toLowerCase() !== "test",
  {
    path: ["title"],
    message: "Withlishkom tekhnicheskoe name for zametki",
  }
);

export type NoteInput = z.infer<typeof noteSchema>;

Dieses Schema definiert gültige Werte, Transformationen (wie trim) und benutzerdefinierte Regeln (refine). Es wird zur einzigen Quelle der Wahrheit für die Validierung.

Sichere Validierung mit safeParse

Die Verwendung von safeParse statt parse ist für Formulare entscheidend. parse wirft bei Fehlern eine Exception – schlecht für die UX, wo ungültige Eingaben erwartet werden. safeParse liefert ein handhabbares Ergebnis:

export function validateNote(input: unknown) {
  return noteSchema.safeParse(input);
}

Das Ergebnis enthält eine success-Flag und bei Bedarf ein error-Objekt. So können UI- und Serverlogik denselben Response-Typ ohne try/catch handhaben.

Google AdInline article slot

Fehler vereinheitlichen mit flatten

Zod liefert ein komplexes Fehlerobjekt. Für Formulare reichen zwei Ebenen: Feldfehler und formularweiter Fehler. Die flatten()-Methode ist ideal:

export function formatZodError(error: import("zod").ZodError) {
  const flat = error.flatten();

  return {
    fieldErrors: flat.fieldErrors,
    formError: flat.formErrors[0] ?? null,
  };
}

Beispiele für Unterschiede:

  • fieldErrors: „Titel darf nicht leer sein“, „Beschreibung überschreitet 500 Zeichen“.
  • formError: „Notiz mit diesem Slug existiert bereits“, „Server vorübergehend nicht verfügbar“.

Diese Trennung macht die UX vorhersehbar: Nutzer wissen genau, wo sie eingreifen müssen und wann sie neu versuchen oder warten sollen.

Einheitlicher Vertrag auf Client und Server

Auf dem Client ermöglicht das Zod-Schema frühe Validierung und UI-Zustandsverwaltung:

const clientResult = useMemo(() => {
  return noteSchema.pick({ title: true }).safeParse({ title });
}, [title]);

const titleError = clientResult.success
  ? null
  : clientResult.error.flatten().fieldErrors.title?.[0] ?? null;

const canSubmit = clientResult.success;

Auf dem Server dient dasselbe Schema als letzte Verteidigungslinie:

export async function createNoteAction(raw: unknown) {
  const parsed = noteSchema.safeParse(raw);

  if (!parsed.success) {
    const flat = parsed.error.flatten();
    return {
      ok: false,
      fieldErrors: flat.fieldErrors,
      formError: flat.formErrors[0] ?? null,
    };
  }

  try {
    // save to database
    return {
      ok: true,
      fieldErrors: {},
      formError: null,
    };
  } catch {
    return {
      ok: false,
      fieldErrors: {},
      formError: "Not succeeded sokhranit zametku",
    };
  }
}

Eine Regel, verschiedene Rollen: UX auf dem Client, Sicherheit auf dem Server.

Standard-FormState-Struktur

Um Fragmentierung zu vermeiden, definiere einen einheitlichen Typ für alle Formulare:

export type FormState = {
  ok: boolean;
  fieldErrors: Record<string, string[] | undefined>;
  formError: string | null;
};

Diese Struktur deckt alle Zustände ab: Erfolg, Felderfehler, allgemeine Fehler. Jedes Formular im Projekt liefert dieselbe Struktur – das vereinfacht die UI-Verarbeitung.

Wann React Hook Form einsetzen

React Hook Form (RHF) ist mächtig, aber kein Allheilmittel. Es lohnt sich bei Formularen mit:

  • Vielen Feldern mit abhängiger Validierung
  • Dynamischen Listen oder verschachtelten Strukturen
  • Blur-Validierung, touched/dirty-Zuständen
  • Benutzerdefinierten Controls mit Fokusverwaltung
  • Bedarf an Render-Optimierung

Bei einfachen Formularen mit 1–2 Feldern und Basisvalidierung bringt RHF unnötige Komplexität. Wenn useMemo + Zod abgeleiteter State reicht – bleib einfach.

Denkweise wechseln: Vom UI zum Vertrag

Der entscheidende Wechsel? Hör auf, Formulare als reine UI-Components zu sehen. Ein Formular ist ein Vertrag, der definiert:

  • Gültiges Datenschema
  • Fehler-Response-Format
  • Verhalten für Zustände (pending, success, error)
  • Verantwortlichkeitsgrenzen zwischen Client und Server

Mit dem Vertrag im Voraus wird die UI-Implementierung mechanisch. Am wichtigsten: Jedes Formular im Projekt verhält sich vorhersehbar, weil es denselben Vertrag einhält.

Wichtige Erkenntnisse

  • Verwende ein Zod-Schema für Client und Server – eliminiert Regelabweichungen.
  • Nutze immer safeParse – es liefert handhabbare Ergebnisse statt Exceptions.
  • Trenne fieldErrors und formError – verbessert UX und vereinfacht Anzeigelogik.
  • Definiere eine standard FormState-Struktur – alle Formulare liefern dieselbe Struktur.
  • Füge React Hook Form nur hinzu, wenn es wirklich nötig ist – nicht aus Trend, sondern für komplexe Szenarien.

— Editorial Team

Advertisement 728x90

Weiterlesen