Use Cases (Aktualisiert: 2.6.2026)

React Hook Form mit Claude Code sicher implementieren

Formulare mit useForm, Zod, Fehlermeldungen, Submit-Status, Tests und sicheren Claude-Code-Prompts bauen.

React Hook Form mit Claude Code sicher implementieren

Erst den Formularvertrag klären, dann Claude Code starten

React Hook Form ist eine schlanke Bibliothek für Formulare in React. Statt jeden Tastendruck in useState zu speichern, nutzt sie das native Formularverhalten des Browsers und stellt APIs wie register, handleSubmit und formState bereit. Gerade für Einsteiger wird dadurch klarer, wo Werte gesammelt werden, wann Validierung läuft und wie ein zweiter Submit während einer laufenden Anfrage verhindert wird.

Claude Code kann Komponente, Zod-Schema, API-Route, Tests und erste Refactorings in einem Arbeitsgang erstellen. Das ist nützlich, aber Formulare liegen oft direkt im Geschäftspfad: Kontaktanfrage, Produkttest, Kaufvorbereitung, Newsletter oder Profilbearbeitung. Wenn der Auftrag nur “baue ein Formular” lautet, entsteht schnell eine hübsche Oberfläche ohne barrierearme Fehlermeldungen, Servervalidierung oder zuverlässigen Ladezustand.

Dieser Leitfaden nutzt ein Anfrageformular und zeigt useForm, zodResolver, Feldfehler, Submit-Status, Server-Revalidierung, Testbarkeit und sichere Prompts für Claude Code. Für allgemeine React-Muster siehe React-Entwicklung mit Claude Code. Für Zod-Schemas passt Zod-Validierung mit Claude Code dazu.

Architektur: Das Schema steht im Zentrum

React Hook Form steuert den Formularablauf. Zod beschreibt, welche Eingaben gültig sind. zodResolver aus @hookform/resolvers/zod verbindet beides, damit React Hook Form beim Validieren das Zod-Schema ausführt.

flowchart TD
  A["Nutzer gibt Daten ein"] --> B["React Hook Form register"]
  B --> C["zodResolver validiert Schema"]
  C --> D{"Eingabe gültig"}
  D -->|Nein| E["Feldfehler anzeigen"]
  D -->|Ja| F["handleSubmit sendet Werte"]
  F --> G["API validiert dasselbe Schema"]
  G --> H["Speichern, benachrichtigen oder CRM syncen"]

Einfach gesagt: useForm ist der Controller, das Zod-Schema ist das Regelwerk, der Resolver ist der Adapter. Wenn du Claude Code um Änderungen bittest, reduziert diese Trennung unnötige Umbauten. Später kannst du gezielt verlangen, dass Schema, Select-Option, API und Tests gemeinsam aktualisiert werden.

Für Details nutze offizielle Quellen: useForm in React Hook Form, React Hook Form Resolvers, Zod API, Reacts <input> Referenz, sowie Claude Code overview und commands.

Kopierbares Zod-Schema

Lege die Validierungsregeln zuerst in eine eigene Datei. Das Beispiel enthält Name, E-Mail, Kategorie, Nachricht und Kontaktzustimmung. z.infer erzeugt den TypeScript-Typ aus dem Schema, sodass Laufzeitvalidierung und Typdefinition nicht auseinanderlaufen.

// src/features/inquiry/inquirySchema.ts
import { z } from "zod";

export const inquirySchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Bitte Namen eingeben")
    .max(80, "Der Name darf höchstens 80 Zeichen haben"),
  email: z
    .string()
    .trim()
    .email("Bitte eine gültige E-Mail-Adresse eingeben"),
  category: z.enum(["consulting", "support", "billing"], {
    error: "Bitte Kategorie auswählen",
  }),
  message: z
    .string()
    .trim()
    .min(10, "Bitte mindestens 10 Zeichen eingeben")
    .max(1000, "Die Nachricht darf höchstens 1000 Zeichen haben"),
  agreeToContact: z.boolean().refine((value) => value, {
    message: "Die Kontaktzustimmung ist erforderlich",
  }),
});

export type InquiryFormValues = z.infer<typeof inquirySchema>;

Die Kategorie ist ein Enum, weil der gesendete Wert stabil bleiben soll. In echten Projekten steuert dieser Wert oft Sales-Routing, Support-Warteschlangen, Rechnungsfragen oder CRM-Felder. Schreibe im Claude-Code-Prompt daher sichtbares Label und gesendeten Wert getrennt auf, etwa “Label: Technischer Support, Wert: support”. Labels können übersetzt werden, Datenwerte sollten stabil bleiben.

useForm für Werte, Fehler und Submit-Status

Die Komponente verbindet das Schema mit React Hook Form. mode: "onBlur" validiert, wenn ein Feld verlassen wird. Für Kontaktformulare ist das oft angenehmer als rote Fehlermeldungen bei jedem Tastendruck. Beim Absenden führt handleSubmit trotzdem die finale Validierung aus.

// src/features/inquiry/InquiryForm.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { inquirySchema, type InquiryFormValues } from "./inquirySchema";

async function sendInquiry(values: InquiryFormValues) {
  const response = await fetch("/api/inquiry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(values),
  });

  if (!response.ok) {
    throw new Error("Failed to send inquiry");
  }
}

export function InquiryForm() {
  const {
    register,
    handleSubmit,
    reset,
    setError,
    formState: { errors, isSubmitting },
  } = useForm<InquiryFormValues>({
    resolver: zodResolver(inquirySchema),
    mode: "onBlur",
    defaultValues: {
      name: "",
      email: "",
      message: "",
      agreeToContact: false,
    },
  });

  const onSubmit = async (values: InquiryFormValues) => {
    try {
      await sendInquiry(values);
      reset();
    } catch {
      setError("root", {
        type: "server",
        message: "Das Formular konnte nicht gesendet werden. Bitte später erneut versuchen.",
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          autoComplete="name"
          aria-invalid={errors.name ? "true" : "false"}
          aria-describedby={errors.name ? "name-error" : undefined}
          {...register("name")}
        />
        {errors.name && (
          <p id="name-error" role="alert">
            {errors.name.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="email">E-Mail</label>
        <input
          id="email"
          type="email"
          autoComplete="email"
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={errors.email ? "email-error" : undefined}
          {...register("email")}
        />
        {errors.email && (
          <p id="email-error" role="alert">
            {errors.email.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="category">Thema</label>
        <select
          id="category"
          aria-invalid={errors.category ? "true" : "false"}
          aria-describedby={errors.category ? "category-error" : undefined}
          {...register("category")}
        >
          <option value="">Bitte wählen</option>
          <option value="consulting">Einführungsberatung</option>
          <option value="support">Technischer Support</option>
          <option value="billing">Rechnung oder Vertrag</option>
        </select>
        {errors.category && (
          <p id="category-error" role="alert">
            {errors.category.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="message">Nachricht</label>
        <textarea
          id="message"
          rows={6}
          aria-invalid={errors.message ? "true" : "false"}
          aria-describedby={errors.message ? "message-error" : undefined}
          {...register("message")}
        />
        {errors.message && (
          <p id="message-error" role="alert">
            {errors.message.message}
          </p>
        )}
      </div>

      <label>
        <input type="checkbox" {...register("agreeToContact")} />
        Ich stimme zu, zu dieser Anfrage kontaktiert zu werden
      </label>
      {errors.agreeToContact && (
        <p role="alert">{errors.agreeToContact.message}</p>
      )}

      {errors.root && <p role="alert">{errors.root.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Wird gesendet..." : "Anfrage senden"}
      </button>
    </form>
  );
}

Wichtig ist die Verbindung zwischen Feld und Fehler. Jedes Feld nutzt aria-invalid, jede Fehlermeldung role="alert", und aria-describedby verknüpft Eingabe und Erklärung. Das hilft Screenreadern und macht Tests robuster. Mehr dazu in Accessibility mit Claude Code.

Auch auf dem Server validieren

Client-Validierung verbessert die UX, schützt aber nicht die API. Eine Anfrage kann direkt an den Server geschickt werden. Darum sollte dieselbe Schema-Definition auch serverseitig mit safeParse laufen, bevor Daten gespeichert oder weitergeleitet werden.

// app/api/inquiry/route.ts
import { NextResponse } from "next/server";
import { inquirySchema } from "@/features/inquiry/inquirySchema";

export async function POST(request: Request) {
  const payload = await request.json().catch(() => null);
  const parsed = inquirySchema.safeParse(payload);

  if (!parsed.success) {
    return NextResponse.json(
      {
        error: "Invalid inquiry",
        fields: parsed.error.flatten().fieldErrors,
      },
      { status: 400 },
    );
  }

  // TODO: In Datenbank speichern, E-Mail senden oder CRM synchronisieren.
  return NextResponse.json({ ok: true });
}

Bitte Claude Code konkret: “inquirySchema wiederverwenden, bei Fehlern 400 mit field errors zurückgeben und echte E-Mail- oder CRM-Integration als TODO lassen.” Secrets, Retries und Deduplizierung sind eigene Aufgaben.

Testbarkeit einbauen

Formulare brechen oft unauffällig. Teste mindestens leeres Absenden, gültiges Absenden, Serverfehler und deaktivierten Button. Mit Vitest und React Testing Library lässt sich prüfen, ob Fehler erscheinen und fetch aufgerufen wird.

// src/features/inquiry/InquiryForm.test.tsx
import { afterEach, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InquiryForm } from "./InquiryForm";

afterEach(() => {
  vi.unstubAllGlobals();
});

test("zeigt Fehler beim leeren Absenden", async () => {
  render(<InquiryForm />);

  await userEvent.click(screen.getByRole("button", { name: "Anfrage senden" }));

  expect(await screen.findAllByRole("alert")).toHaveLength(5);
});

test("sendet gültige Werte an die API", async () => {
  const fetchMock = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
  vi.stubGlobal("fetch", fetchMock);
  render(<InquiryForm />);

  await userEvent.type(screen.getByLabelText("Name"), "Masa");
  await userEvent.type(screen.getByLabelText("E-Mail"), "masa@example.com");
  await userEvent.selectOptions(screen.getByLabelText("Thema"), "consulting");
  await userEvent.type(
    screen.getByLabelText("Nachricht"),
    "Ich möchte React Hook Form zuverlässig einführen.",
  );
  await userEvent.click(
    screen.getByLabelText("Ich stimme zu, zu dieser Anfrage kontaktiert zu werden"),
  );
  await userEvent.click(screen.getByRole("button", { name: "Anfrage senden" }));

  expect(fetchMock).toHaveBeenCalledWith(
    "/api/inquiry",
    expect.objectContaining({ method: "POST" }),
  );
});

Du kannst Claude Code bitten, zuerst fehlende Tests zu schreiben und danach die Implementierung anzupassen. Für Browser-Flows passt Playwright Testing mit Claude Code. Für Business-Messung sollte das Analytics-Event nach erfolgreicher Serverannahme laufen, nicht beim Klick; siehe Analytics mit Claude Code.

Sicherer Prompt für Claude Code

Ein guter Prompt enthält Scope, Constraints, Verifikation und Nicht-Ziele.

Implementiere ein Anfrageformular mit React Hook Form und Zod.

Scope:
- Nur src/features/inquiry und app/api/inquiry ändern.
- useForm, zodResolver und TypeScript-Typen aus dem Schema verwenden.
- Felder: name, email, category, message, agreeToContact.
- Field errors mit role="alert" und aria-describedby anzeigen.
- Submit-Button deaktivieren, solange isSubmitting true ist.
- In der API dasselbe Zod-Schema mit safeParse validieren.
- Vitest + Testing Library Tests hinzufügen.

Verifikation:
- npm test -- InquiryForm
- npm run typecheck

Nicht tun:
- Keine neue UI-Bibliothek hinzufügen.
- Bestehende category-Werte nicht umbenennen.
- Keine echte E-Mail-, CRM- oder Secret-Logik in dieser Aufgabe.

Auch kleine Änderungen sollten präzise sein. Statt “füge eine Kategorie hinzu” schreibe: “Label Schulungsanfrage, Wert training, und aktualisiere enum, select, API-Validierung, Tests und Analytics-Mapping.” Claude Code kann Dateien suchen, aber der Vertrag kommt von dir.

Einsatzfälle und Unterschiede

EinsatzfallGute StrukturAchtung
KontaktformularZod + React Hook Form + API-RevalidierungErfolgreiche Leads messen, nicht Klicks
ProfilbearbeitungDaten in defaultValues ladenNach dem Speichern reset(savedValues) aufrufen
Kaufvorbereitende UmfrageSelect, Radio und Checkbox kombinierenWerte mit Produkt- oder CRM-IDs abgleichen
Admin-SucheLeichte Validierung und URL QueryNicht bei jedem Tastendruck API aufrufen

Die gemeinsame Regel: sichtbares Label und gesendeter Wert sind getrennt. Labels dürfen übersetzt werden. Gesendete Werte müssen stabil bleiben, weil Reports, Automationen und Backend davon abhängen.

Häufige Fehler

Erstens: nur im Browser validieren. Importiere das geteilte Schema in die API und rufe safeParse auf, bevor du den Payload verarbeitest.

Zweitens: isSubmitting geht zu früh zurück. Wenn onSubmit die asynchrone Arbeit nicht awaitet, wird der Button zu früh wieder aktiv.

Drittens: Fehler stehen zu weit vom Feld entfernt. Eine globale Meldung kann helfen, ersetzt aber nicht die konkrete Nachricht direkt am Feld.

Viertens: Claude Code erfindet ein neues Designsystem. Wenn es schon TextField, Select, Button oder Toasts gibt, muss der Prompt deren Nutzung verlangen.

Fünftens: der Weg nach dem Submit fehlt. Erfolgsmeldung, Danke-Seite, E-Mail, Analytics-Event und CRM-Sync sollten bewusst geplant sein.

Monetarisierungs-CTA

Formularqualität misst sich daran, welchen Funnel sie stützt: PDF-Download, Produktlead, Template-Kauf oder Beratungsgespräch. Definiere zuerst dieses Ereignis, dann lasse Claude Code Felder reduzieren, Texte verbessern oder Tests ergänzen.

Zum Selbstlernen findest du Materialien auf der Produktseite. Für Team-Einführung und Review-Prozesse eignet sich Training und Beratung. Ein Formular ist klein, aber oft der Übergang von Inhalt zu Umsatz.

Getestetes Ergebnis

Masa hat diese Struktur in einem kleinen Anfragefluss getestet. Der größte Gewinn war das zentrale Schema, weil dadurch der Fehler vermieden wurde, eine Select-Option in der UI zu ergänzen, aber den erlaubten API-Wert zu vergessen. Die Tests für leeres und gültiges Absenden halfen ebenfalls. Nach Claude-Code-Änderungen fanden sie fehlende Fehlermeldungen und einen defekten fetch-Aufruf schnell. In der Praxis ist ein Formular als Eingabevertrag wartbarer als ein Formular als reine UI.

#Claude Code #React Hook Form #React #Formulare #Validierung
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.