Use Cases (Aktualisiert: 2.6.2026)

Zod-Validierung mit Claude Code in TypeScript

Baue mit Claude Code und Zod sichere Formulare, APIs, Env-Checks, Webhooks und Tests.

Zod-Validierung mit Claude Code in TypeScript

Warum Zod an Runtime-Grenzen gehört

TypeScript hilft beim Schreiben von Code, prüft aber nicht automatisch Daten, die zur Laufzeit von außen kommen. Ein Browserformular, ein API Body, ein Webhook payload, process.env oder ein Objekt direkt vor dem Datenbank-Insert kann eine andere Form haben als erwartet. Runtime-Validierung bedeutet, diese Werte vor der Business-Logik zu prüfen. Zod beschreibt dafür ein schema, also einen Datenvertrag, und leitet daraus den TypeScript-Typ ab.

Claude Code ist für Zod nützlich, weil Validierung stark strukturiert ist: Felder, Typen, Grenzen, Fehlermeldungen, Einsatzorte und Tests. Eine vage Bitte wie “baue Validierung” erzeugt oft zu wenig. Eine präzise Bitte wie “Formular, API request/response, Environment Variables, Webhook payload, Prüfung vor DB Insert und Tests” gibt Claude Code die nötigen Grenzen. Details sollten mit der Zod Dokumentation und der Next.js Route Handlers Dokumentation abgeglichen werden.

unknown input
  -> Zod schema
  -> safeParse
  -> typed data
  -> business logic
  -> response schema
  -> client

Der wichtigste Schritt ist der Anfang. Externe Daten bleiben unknown, bis Zod sie akzeptiert. Ein Type Cast mit as SomeType ist kein Sicherheitsnachweis.

Use Cases für Claude Code

Jede Eingangsgrenze braucht eine eigene Strategie. Ein Formular braucht gute Nutzermeldungen, eine API braucht saubere 400 Antworten, Environment Variables sollen beim Start fehlschlagen, und ein Webhook muss zuerst die Signatur prüfen.

Use CaseEingangWas Zod schützt
FormularBrowserdatenLeere Felder, E-Mail, Länge, Zustimmung
API request/responserequest.json() und Antwort JSONUngültiger Payload, Antwortvertrag, Statuswerte
Environment Variablesprocess.envFehlende Secrets, URLs, Ports
Webhook payloadPOST eines DrittanbietersEvent-Typ, Objekt-IDs, Beträge, Signaturfluss
Vor DB InsertTransformiertes App-ObjektSpeicherbare Felder, Enums, Pflicht-IDs

Gib Claude Code diese Tabelle. Ein einziges schema für alle Schichten führt schnell zu vermischten Verantwortlichkeiten. Ein Formular kann Bestätigungsfelder enthalten; ein DB-Insert braucht andere Felder. Teile kleine Bausteine wie emailSchema oder idSchema, aber nicht blind ganze Objekte. Für Formulare passt der interne React Hook Form Guide. Für API-Typisierung siehe auch tRPC Entwicklung.

Ein Basis-Schema schreiben

Dieses Beispiel validiert ein Kontaktformular. Es ist bewusst kopierbar gehalten. Bitte Claude Code um nutzerfreundliche Fehlermeldungen und darum, keine Datenbankfelder wie id oder createdAt in das Formularschema einzubauen.

// src/lib/schemas/contact.ts
import { z } from "zod";

export const contactFormSchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Bitte Namen eingeben")
    .max(80, "Der Name darf höchstens 80 Zeichen lang sein"),
  email: z
    .string()
    .trim()
    .email("Bitte eine gültige E-Mail-Adresse eingeben"),
  plan: z.enum(["trial", "team", "enterprise"]),
  message: z
    .string()
    .trim()
    .min(10, "Die Nachricht muss mindestens 10 Zeichen lang sein")
    .max(2000, "Die Nachricht darf höchstens 2000 Zeichen lang sein"),
  agreedToPolicy: z
    .boolean()
    .refine((value) => value, "Die Datenschutzerklärung muss akzeptiert werden"),
});

export type ContactFormInput = z.infer<typeof contactFormSchema>;

trim() verhindert, dass nur Leerzeichen akzeptiert werden. z.enum begrenzt Strings auf bekannte Werte. z.infer reduziert doppelte Pflege von Schema und Interface.

safeParse in API-Fehler umwandeln

parse wirft bei einem Fehler eine Exception. Das ist für Startkonfiguration geeignet. safeParse liefert ein kontrolliertes Ergebnis und ist für Formular- und API-Fehler besser.

// src/lib/validation.ts
import { z } from "zod";

export type ValidationProblem = {
  path: string;
  message: string;
};

export function validateInput<TSchema extends z.ZodTypeAny>(
  schema: TSchema,
  input: unknown,
):
  | { ok: true; data: z.infer<TSchema> }
  | { ok: false; status: 400; errors: ValidationProblem[] } {
  const result = schema.safeParse(input);

  if (!result.success) {
    return {
      ok: false,
      status: 400,
      errors: result.error.issues.map((issue) => ({
        path: issue.path.join(".") || "_root",
        message: issue.message,
      })),
    };
  }

  return { ok: true, data: result.data };
}

Dieser Helper standardisiert Fehlermeldungen. In einer mehrsprachigen App kann er statt Texten message keys zurückgeben.

Request und Response in Next.js validieren

Eine Route sollte Eingaben vor der Business-Logik validieren. Auch die Antwort zu validieren lohnt sich, weil ein versehentlich geänderter öffentlicher Vertrag sofort auffällt.

// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";
import { validateInput } from "@/lib/validation";

const contactResponseSchema = z.object({
  id: z.string().min(1),
  status: z.enum(["queued"]),
});

async function saveContact(input: ContactFormInput) {
  // Replace this with your database insert.
  return {
    id: `contact_${Date.now()}`,
    status: "queued" as const,
    email: input.email,
  };
}

export async function POST(request: Request) {
  const body: unknown = await request.json();
  const validated = validateInput(contactFormSchema, body);

  if (!validated.ok) {
    return NextResponse.json(
      { message: "Bitte Eingaben prüfen", errors: validated.errors },
      { status: validated.status },
    );
  }

  const saved = await saveContact(validated.data);
  const response = contactResponseSchema.parse(saved);

  return NextResponse.json(response, { status: 201 });
}

Bei Webhooks gilt: Signatur zuerst, Payload-Schema danach, Business-Logik zuletzt. Bitte Claude Code, verifySignature, webhookPayloadSchema und handleWebhookEvent getrennt zu halten.

Environment Variables beim Start prüfen

Environment Variables sind Strings oder undefined. Wenn DATABASE_URL fehlt, soll die App beim Start scheitern, nicht erst bei der ersten echten Anfrage.

// src/env.ts
import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
  DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
  NEXT_PUBLIC_APP_URL: z.string().url("NEXT_PUBLIC_APP_URL must be a valid URL"),
  WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 chars"),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error(
    "Invalid environment variables",
    parsed.error.flatten().fieldErrors,
  );
  throw new Error("Invalid environment variables");
}

export const env = parsed.data;

z.coerce.number() passt hier, weil die Quelle sicher stringbasiert ist. Nutze coercion nicht pauschal für jeden JSON Body, sonst werden unsaubere Eingaben still akzeptiert.

Zod mit react-hook-form verbinden

Client-Validierung verbessert die Bedienung. Sie ersetzt aber nie die Server-Validierung, weil ein Browser umgangen werden kann.

// src/components/contact-form.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
  contactFormSchema,
  type ContactFormInput,
} from "@/lib/schemas/contact";

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactFormInput>({
    resolver: zodResolver(contactFormSchema),
    defaultValues: {
      name: "",
      email: "",
      plan: "trial",
      message: "",
      agreedToPolicy: false,
    },
  });

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

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} aria-invalid={Boolean(errors.name)} />
      {errors.name && <p>{errors.name.message}</p>}

      <input {...register("email")} aria-invalid={Boolean(errors.email)} />
      {errors.email && <p>{errors.email.message}</p>}

      <select {...register("plan")}>
        <option value="trial">Trial</option>
        <option value="team">Team</option>
        <option value="enterprise">Enterprise</option>
      </select>

      <textarea {...register("message")} />
      {errors.message && <p>{errors.message.message}</p>}

      <label>
        <input type="checkbox" {...register("agreedToPolicy")} />
        I agree to the privacy policy
      </label>
      {errors.agreedToPolicy && <p>{errors.agreedToPolicy.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        Send
      </button>
    </form>
  );
}

Wenn Claude Code an der UI arbeitet, gib explizit vor, dass die Server-Prüfung nicht entfernt werden darf. Sonst sieht das Formular besser aus, während die API schwächer wird.

Review-Prompt für Claude Code

Nach der Implementierung sollte Claude Code gezielt reviewen. Der Prompt muss eng sein.

Review only the Zod validation design in these files.

Check:
1. Every external input is treated as unknown until safeParse or parse succeeds.
2. Route Handlers return 400 with field-level errors for invalid requests.
3. Environment variables fail fast at startup.
4. coerce and transform are used only where the input source is clear.
5. Form schemas, API schemas, and database insert schemas are not over-shared.
6. Error messages are user-facing and ready for localization.

Do not refactor unrelated business logic.
Return findings with file paths, risk level, and a minimal patch suggestion.

Damit werden die wichtigsten Risiken geprüft: TypeScript-Typen statt Runtime-Validierung, falscher Einsatz von parse, zu viel coerce, Nebenwirkungen in transform, fehlende Lokalisierung und zu breite Schema-Wiederverwendung.

Vertrag mit Tests sichern

Ein schema ist ein Produktvertrag. Teste einen gültigen Fall und mehrere ungültige Fälle, damit spätere Änderungen die Regeln nicht unbemerkt lockern.

// src/lib/schemas/contact.test.ts
import { describe, expect, it } from "vitest";
import { contactFormSchema } from "./contact";

describe("contactFormSchema", () => {
  it("accepts a valid contact request", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "masa@example.com",
      plan: "team",
      message: "I want to introduce Claude Code to my team.",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(true);
  });

  it("rejects invalid email and short message", () => {
    const result = contactFormSchema.safeParse({
      name: "Masa",
      email: "not-an-email",
      plan: "team",
      message: "short",
      agreedToPolicy: true,
    });

    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
        expect.arrayContaining(["email", "message"]),
      );
    }
  });
});

Für DB-Insert-Validierung sollte das finale Objekt direkt vor dem Speichern getestet werden. So fallen geänderte Feldnamen, Enums oder Transformationen auf.

Häufige Fallen

Erstens: TypeScript-Typen allein sind keine Runtime-Validierung. request.json() as ContactFormInput überzeugt nur den Compiler.

Zweitens: parse und safeParse nicht vermischen. Nutzereingaben brauchen meist safeParse und 400 Antworten. Startup-Konfiguration darf den Prozess stoppen.

Drittens: coerce nicht übertreiben. Es ist gut für Query Strings und Environment Variables, aber riskant bei unklaren JSON-Eingaben.

Viertens: Keine Nebenwirkungen in transform. Datenbankzugriffe, E-Mails und Analytics gehören nach der Validierung.

Fünftens: Fehlermeldungen und Lokalisierung früh planen. Mehrsprachige Produkte brauchen message keys oder eine zentrale Mapping-Schicht.

Sechstens: Schemas nicht nur aus Bequemlichkeit teilen. Formular, API, Webhook und DB Insert sind ähnliche, aber unterschiedliche Verträge.

Beratung und Verifikationsnotiz

Wenn Validierung bereits über Formulare, Webhooks, API-Routen und DB-Code verstreut ist, kann Claude Code Lab bei Schema-Schichten, Review-Prompts und Tests helfen. Für Projektbegleitung eignet sich die englische Seite für Consulting und Training.

Die Beispiele wurden am 2026-06-02 gegen die offiziellen Zod- und Next.js-Route-Handler-Dokumente geprüft. Sie setzen ein TypeScript-Projekt mit zod, react-hook-form, @hookform/resolvers und vitest voraus. In echten Projekten ergänzt Masa Authentifizierung, CSRF oder Webhook-Signaturprüfung, DB-Constraints und Fehlertests für jede externe Grenze.

#Claude Code #Zod #validation #TypeScript #type safety
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.