1. Zod Grundlagen: Primitive Typen und Type Inference
Zod ist eine TypeScript-first Schema-Validierungsbibliothek, die zur Laufzeit validiert und gleichzeitig statische TypeScript-Typen ableitet. Das Besondere: Du definierst dein Schema einmal — Zod kümmert sich um beides.
Claude Code generiert Zod-Schemas direkt aus deinen Anforderungen: "Ich brauche ein Schema für einen User mit optionaler Bio" — und Claude liefert sofort produktionsreifen Code mit korrekter Type Inference, passenden Fehlermeldungen und Best Practices.
Grundtypen Primitive Schemas und Basis-Operationen
Die wichtigsten Zod-Primitiven decken alle Standard-TypeScript-Typen ab:
import { z } from "zod";
// Primitive Typen
const nameSchema = z.string().min(2).max(100);
const ageSchema = z.number().int().min(0).max(150);
const activeSchema = z.boolean();
const birthDateSchema = z.date();
const scoreSchema = z.number().nonnegative().multipleOf(0.5);
// String-spezifische Validierungen
const emailSchema = z.string().email("Keine gültige E-Mail-Adresse");
const urlSchema = z.string().url("Keine gültige URL");
const uuidSchema = z.string().uuid();
const slugSchema = z.string().regex(/^[a-z0-9-]+$/, "Nur Kleinbuchstaben, Zahlen und Bindestriche");
// Object-Schema
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().positive().optional(),
bio: z.string().max(500).nullable(),
createdAt: z.date(),
});
// Type Inference — kein doppelter Interface-Code nötig!
type User = z.infer<typeof userSchema>;
// { id: string; name: string; email: string; age?: number; bio: string | null; createdAt: Date }
// Array-Schema
const userListSchema = z.array(userSchema).min(1).max(100);
type UserList = z.infer<typeof userListSchema>; // User[]
// Enum-Schema
const roleSchema = z.enum(["admin", "editor", "viewer"]);
type Role = z.infer<typeof roleSchema>; // "admin" | "editor" | "viewer"
// Validierung ausführen
const result = userSchema.safeParse({
id: "550e8400-e29b-41d4-a716-446655440000",
name: "Max Mustermann",
email: "max@example.com",
bio: null,
createdAt: new Date(),
});
if (result.success) {
// result.data ist vollständig typisiert als User
console.log(result.data.email); // TypeScript kennt den Typ
} else {
// result.error enthält strukturierte Fehlermeldungen
console.error(result.error.flatten());
}
Claude Code Tipp: Beschreibe dein Datenmodell in natürlicher Sprache — "Ein Produkt hat einen Namen, optionale Beschreibung, Pflichtpreis als Dezimalzahl und eine Kategorie aus vordefinierten Werten" — Claude generiert daraus ein vollständiges Zod-Schema mit korrekter Type Inference und sinnvollen Validierungsgrenzen.
Ein zentraler Vorteil von Zod gegenüber reinen TypeScript-Interfaces: Die Typen existieren nicht nur zur Compile-Zeit, sondern werden auch zur Laufzeit durchgesetzt. API-Responses, Formulardaten oder externe Konfigurationen — alles wird sicher validiert, bevor dein Code damit arbeitet.
Type Inference Schemas als Single Source of Truth
Zod macht dein Schema zur einzigen Wahrheitsquelle — kein doppelter Typ-Code mehr:
// VORHER (ohne Zod): Doppelter Code, schnell out-of-sync
interface Product {
id: string;
name: string;
price: number;
category: "electronics" | "clothing" | "food";
}
// Separate Validierungslogik nötig...
// NACHHER (mit Zod): Schema = Typ + Validierung in einem
const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(200),
price: z.number().positive().multipleOf(0.01),
category: z.enum(["electronics", "clothing", "food"]),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string()).optional(),
});
type Product = z.infer<typeof productSchema>;
// Abgeleiteter Typ ist immer synchron mit dem Schema!
// Partial und Pick für abgeleitete Schemas
const createProductSchema = productSchema.omit({ id: true });
const updateProductSchema = productSchema.partial().required({ id: true });
const productSummarySchema = productSchema.pick({ id: true, name: true, price: true });
type CreateProduct = z.infer<typeof createProductSchema>;
type UpdateProduct = z.infer<typeof updateProductSchema>;
type ProductSummary = z.infer<typeof productSummarySchema>;
2. Komplexe Schemas: Union, Intersection und Transform
Echte Anwendungen haben selten einfache, flache Datenstrukturen. Zod bietet mächtige Kompositionswerkzeuge für verschachtelte, bedingte und transformierende Schemas.
Claude Code ist besonders stark bei der Generierung komplexer Schemas für realistische Use Cases: unterschiedliche Payload-Typen je nach Ereignis, verschachtelte Objekte mit gegenseitigen Abhängigkeiten oder Datenstrukturen, die validiert und gleichzeitig transformiert werden müssen.
Union z.union() und z.discriminatedUnion()
// z.union() — für beliebige Vereinigungstypen
const idSchema = z.union([z.string().uuid(), z.number().int().positive()]);
type Id = z.infer<typeof idSchema>; // string | number
// z.discriminatedUnion() — performanter für Tagged Unions
const paymentSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("credit_card"),
cardNumber: z.string().length(16),
expiryMonth: z.number().min(1).max(12),
expiryYear: z.number().min(2026),
cvv: z.string().length(3),
}),
z.object({
type: z.literal("paypal"),
email: z.string().email(),
paypalToken: z.string(),
}),
z.object({
type: z.literal("bank_transfer"),
iban: z.string().regex(/^[A-Z]{2}\d{2}[A-Z0-9]+$/),
bic: z.string().min(8).max(11),
}),
]);
type Payment = z.infer<typeof paymentSchema>;
// TypeScript narrowt automatisch nach Validierung:
function processPayment(data: Payment) {
switch (data.type) {
case "credit_card":
console.log(data.cardNumber); // TypeScript weiß: cardNumber existiert
break;
case "paypal":
console.log(data.email); // TypeScript weiß: email existiert
break;
}
}
Transform z.transform() — Validierung und Transformation in einem
// Transform: Eingabe validieren und umformen
const dateStringSchema = z.string()
.datetime()
.transform((str) => new Date(str));
// Input: string → Output: Date
type ParsedDate = z.infer<typeof dateStringSchema>; // Date
// Komplexe Transformation: API-Response normalisieren
const apiUserSchema = z.object({
user_id: z.string(),
first_name: z.string(),
last_name: z.string(),
email_address: z.string().email(),
created_at: z.string().datetime(),
}).transform((raw) => ({
id: raw.user_id,
fullName: `${raw.first_name} ${raw.last_name}`,
email: raw.email_address,
createdAt: new Date(raw.created_at),
}));
type NormalizedUser = z.infer<typeof apiUserSchema>;
// { id: string; fullName: string; email: string; createdAt: Date }
// z.intersection() für Typ-Kombinationen
const baseEntitySchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
// .merge() — eleganter als z.intersection() für Objekte
const articleSchema = baseEntitySchema.merge(z.object({
title: z.string().min(5),
content: z.string().min(100),
publishedAt: z.date().optional(),
tags: z.array(z.string()),
}));
const commentSchema = baseEntitySchema.merge(z.object({
content: z.string().min(1).max(2000),
authorId: z.string().uuid(),
articleId: z.string().uuid(),
likes: z.number().nonnegative().default(0),
}));
Claude Code Praxis-Tipp: Nutze z.discriminatedUnion() statt z.union() wann immer du ein gemeinsames Discriminator-Feld hast. Zod kann dann direkt den richtigen Schema-Zweig auswählen, ohne alle Varianten auszuprobieren — deutlich bessere Performance bei der Validierung.
3. Custom Validators und Refinements
Wenn Standard-Validierungen nicht ausreichen, bietet Zod mit .refine() und .superRefine() volle Flexibilität für beliebig komplexe Business-Logik-Validierungen.
Claude Code kennt die Feinheiten zwischen .refine() (einfache boolsche Prüfungen) und .superRefine() (mehrere Fehler, Pfad-spezifische Fehlermeldungen). Das ist wichtig, weil bei falscher Wahl entweder Fehlermeldungen fehlen oder die UX leidet.
Custom .refine() für einfache Custom-Validierungen
// .refine() — gibt true zurück wenn valide
const passwordSchema = z.string()
.min(8, "Mindestens 8 Zeichen")
.refine((pw) => /[A-Z]/.test(pw), {
message: "Mindestens ein Großbuchstabe erforderlich",
})
.refine((pw) => /[0-9]/.test(pw), {
message: "Mindestens eine Ziffer erforderlich",
})
.refine((pw) => /[^A-Za-z0-9]/.test(pw), {
message: "Mindestens ein Sonderzeichen erforderlich",
});
// Cross-Field Validierung: Passwort-Bestätigung
const registerSchema = z.object({
email: z.string().email(),
password: passwordSchema,
passwordConfirm: z.string(),
agreeToTerms: z.boolean(),
})
.refine((data) => data.password === data.passwordConfirm, {
message: "Passwörter stimmen nicht überein",
path: ["passwordConfirm"], // Fehler am richtigen Feld anzeigen
})
.refine((data) => data.agreeToTerms === true, {
message: "Du musst den AGB zustimmen",
path: ["agreeToTerms"],
});
// Async Refinement — z.B. Datenbankprüfung
const uniqueEmailSchema = z.string().email().refine(
async (email) => {
const existing = await db.users.findOne({ email });
return !existing;
},
{ message: "E-Mail-Adresse bereits vergeben" }
);
// Wichtig: uniqueEmailSchema.parseAsync() statt parse() verwenden!
SuperRefine .superRefine() für komplexe Multi-Error-Validierungen
// .superRefine() gibt volle Kontrolle über den Zod-Context
const invoiceSchema = z.object({
items: z.array(z.object({
name: z.string(),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
})).min(1),
discount: z.number().min(0).max(100).default(0),
paymentDueDate: z.date(),
paymentMethod: z.enum(["invoice", "prepaid", "subscription"]),
}).superRefine((data, ctx) => {
// Mehrere unabhängige Fehler gleichzeitig hinzufügen
const total = data.items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
if (data.discount > 20 && total < 1000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Rabatt über 20% nur ab Bestellwert €1.000",
path: ["discount"],
});
}
const minDueDays = data.paymentMethod === "invoice" ? 14 : 0;
const daysDiff = (data.paymentDueDate.getTime() - Date.now()) / 86400000;
if (daysDiff < minDueDays) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Zahlungsziel muss mindestens ${minDueDays} Tage in der Zukunft liegen`,
path: ["paymentDueDate"],
});
}
// Doppelte Artikel-Namen prüfen
const names = data.items.map((i) => i.name);
names.forEach((name, idx) => {
if (names.indexOf(name) !== idx) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Artikel "${name}" ist doppelt vorhanden`,
path: ["items", idx, "name"],
});
}
});
});
// Custom Error Map — Deutsche Fehlermeldungen global setzen
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === "string")
return { message: `Mindestens ${issue.minimum} Zeichen erforderlich` };
if (issue.type === "number")
return { message: `Wert muss mindestens ${issue.minimum} betragen` };
}
if (issue.code === z.ZodIssueCode.invalid_type && issue.received === "undefined")
return { message: "Pflichtfeld" };
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);
Wichtig: .refine() bricht die Validierungskette ab sobald es fehlschlägt — nachfolgende .refine()-Aufrufe laufen nicht mehr. Wenn du mehrere unabhängige Validierungen benötigst, die alle gleichzeitig Fehler melden sollen, nutze .superRefine() und rufe ctx.addIssue() für jeden Fehler auf.
4. API-Request-Validation mit Express und Next.js
Zod ist die ideale Lösung für API-Validierung: Als Middleware in Express oder als Route Handler in Next.js schützt es deinen Backend-Code zuverlässig vor ungültigen Eingaben.
Claude Code generiert vollständige Middleware-Setups inklusive Fehlerbehandlung, HTTP-Status-Codes und strukturierten Error-Responses. Das Ergebnis ist eine konsistente API-Validation-Schicht, die mit minimalem Boilerplate auskommt.
API Express.js Middleware mit Zod
import express, { Request, Response, NextFunction } from "express";
import { z, ZodSchema } from "zod";
// Generische Middleware-Factory für Body-Validierung
function validate<T>(schema: ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: "Validierungsfehler",
details: result.error.flatten().fieldErrors,
});
}
req.body = result.data; // Validierte + gecastete Daten
next();
};
}
// Query-Parameter Validierung (immer Strings → z.coerce für Konvertierung)
function validateQuery<T>(schema: ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({
error: "Ungültige Query-Parameter",
details: result.error.flatten().fieldErrors,
});
}
req.query = result.data as any;
next();
};
}
// Schemas für die API-Endpunkte
const createUserBodySchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]).default("viewer"),
});
const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(["name", "createdAt", "email"]).default("createdAt"),
order: z.enum(["asc", "desc"]).default("desc"),
});
// Router mit validierten Routen
const router = express.Router();
router.post("/users", validate(createUserBodySchema), async (req, res) => {
const { name, email, role } = req.body; // vollständig typisiert
const user = await userService.create({ name, email, role });
res.status(201).json(user);
});
router.get("/users", validateQuery(paginationSchema), async (req, res) => {
const { page, limit, sortBy, order } = req.query;
const users = await userService.findAll({ page, limit, sortBy, order });
res.json(users);
});
Next.js App Router Route Handler mit Zod
// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const createProductSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
category: z.enum(["electronics", "clothing", "food"]),
stock: z.number().int().nonnegative(),
});
// Hilfsfunktion für konsistente Error-Responses
function validationError(error: z.ZodError) {
return NextResponse.json(
{
error: "Ungültige Eingabedaten",
issues: error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
},
{ status: 422 }
);
}
export async function POST(req: NextRequest) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Ungültiges JSON" }, { status: 400 });
}
const result = createProductSchema.safeParse(body);
if (!result.success) return validationError(result.error);
const product = await db.products.create(result.data);
return NextResponse.json(product, { status: 201 });
}
// Response-Schemas für vollständige End-to-End-Typisierung
const productResponseSchema = createProductSchema.extend({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type ProductResponse = z.infer<typeof productResponseSchema>;
z.coerce für Query-Parameter: Query-Parameter kommen immer als Strings an. z.coerce.number() konvertiert automatisch "42" zu 42 — ohne manuelles Parsing. Das spart viel Boilerplate bei Paginierung, Filtern und Sortierparametern.
5. React Hook Form + Zod Integration
React Hook Form mit zodResolver ist die leistungsstärkste Kombination für typsichere Formularvalidierung in React. Ein einziges Zod-Schema deckt sowohl Frontend-Validierung als auch Backend-Validierung ab.
Claude Code kennt das komplette Setup: zodResolver einbinden, Field-Level-Validierung konfigurieren, bedingte Felder und dynamische Arrays mit useFieldArray verwalten — und das alles vollständig typisiert ohne ein einziges explizites any.
React Hook Form Vollständiges typsicheres Formular
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// Schema definiert Form-Datenstruktur + Validierungsregeln
const orderFormSchema = z.object({
customer: z.object({
name: z.string().min(2, "Name zu kurz"),
email: z.string().email("Ungültige E-Mail"),
phone: z.string().regex(/^\+?[0-9\s-]{8,}$/, "Ungültige Telefonnummer").optional(),
}),
items: z.array(z.object({
productId: z.string().uuid("Produkt auswählen"),
quantity: z.number({ invalid_type_error: "Menge ist erforderlich" })
.int().positive("Menge muss positiv sein"),
note: z.string().max(200).optional(),
})).min(1, "Mindestens ein Produkt hinzufügen"),
shippingAddress: z.object({
street: z.string().min(5),
city: z.string().min(2),
postalCode: z.string().regex(/^\d{5}$/, "5-stellige PLZ erforderlich"),
country: z.string().length(2).default("DE"),
}),
paymentMethod: z.enum(["invoice", "credit_card", "paypal"]),
notes: z.string().max(500).optional(),
});
type OrderFormData = z.infer<typeof orderFormSchema>;
export function OrderForm() {
const {
register,
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<OrderFormData>({
resolver: zodResolver(orderFormSchema),
defaultValues: {
items: [{ productId: "", quantity: 1 }],
shippingAddress: { country: "DE" },
paymentMethod: "invoice",
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
const onSubmit = async (data: OrderFormData) => {
// data ist vollständig typisiert und validiert
await api.orders.create(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("customer.name")} />
{errors.customer?.name && <p>{errors.customer.name.message}</p>}
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
/>
{errors.items?.[index]?.quantity &&
<p>{errors.items[index].quantity?.message}</p>}
<button type="button" onClick={() => remove(index)}>Entfernen</button>
</div>
))}
<button type="button" onClick={() => append({ productId: "", quantity: 1 })}>
Produkt hinzufügen
</button>
<button type="submit" disabled={isSubmitting}>Bestellung absenden</button>
</form>
);
}
Shared Schema Ein Schema für Frontend und Backend
// shared/schemas/order.ts — von Frontend UND Backend importiert
export const createOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
})).min(1),
});
export type CreateOrderInput = z.infer<typeof createOrderSchema>;
// Frontend: React Hook Form verwendet dasselbe Schema
import { createOrderSchema, CreateOrderInput } from "@/shared/schemas/order";
const form = useForm<CreateOrderInput>({
resolver: zodResolver(createOrderSchema),
});
// Backend: Express/Next.js verwendet dasselbe Schema
import { createOrderSchema } from "@/shared/schemas/order";
const result = createOrderSchema.safeParse(req.body);
// Änderung der Validierung wirkt automatisch auf BEIDE Seiten!
// TypeScript zeigt alle betroffenen Stellen wenn sich das Schema ändert.
valueAsNumber für Zahlenfelder: HTML-Input-Elemente liefern immer Strings. Verwende register("quantity", { valueAsNumber: true }) damit React Hook Form die Konvertierung übernimmt — dann passt der Typ zum z.number()-Schema ohne manuelle Coercion.
6. Zod + tRPC: End-to-End Type Safety
tRPC kombiniert mit Zod ist der Goldstandard für End-to-End Type Safety in TypeScript-Anwendungen. Das Frontend kennt exakt die API-Typen des Backends — ohne Code-Generierung, ohne OpenAPI-Spec, ohne manuelle Synchronisierung.
Claude Code generiert vollständige tRPC-Router mit Input/Output-Schemas, Middleware für Authentifizierung und Autorisierung, sowie typsichere Client-Integrationen. Das Ergebnis: Wenn du die Backend-API änderst, schlägt das TypeScript-Kompilieren auf dem Frontend fehl — sofortiges Feedback, null manuelle Abstimmung.
tRPC Router mit Zod Input und Output Schemas
// server/trpc/router/users.ts
import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
// Input-Schemas
const getUsersInput = z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(50).default(20),
search: z.string().optional(),
role: z.enum(["admin", "editor", "viewer"]).optional(),
});
const createUserInput = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(["editor", "viewer"]).default("viewer"),
sendWelcomeEmail: z.boolean().default(true),
});
const updateUserInput = z.object({
id: z.string().uuid(),
data: createUserInput.partial().omit({ sendWelcomeEmail: true }),
});
// Output-Schemas (validieren auch Backend-Responses!)
const userOutputSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
createdAt: z.date(),
});
const paginatedUsersSchema = z.object({
users: z.array(userOutputSchema),
total: z.number().int(),
page: z.number().int(),
totalPages: z.number().int(),
});
export const usersRouter = router({
getAll: publicProcedure
.input(getUsersInput)
.output(paginatedUsersSchema)
.query(async ({ input }) => {
const { page, limit, search, role } = input;
const [users, total] = await db.users.findAndCount({
where: {
...(search && { name: { contains: search } }),
...(role && { role }),
},
skip: (page - 1) * limit,
take: limit,
});
return { users, total, page, totalPages: Math.ceil(total / limit) };
}),
create: protectedProcedure
.input(createUserInput)
.output(userOutputSchema)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Nur Admins dürfen User erstellen",
});
}
const existing = await db.users.findOne({ email: input.email });
if (existing) {
throw new TRPCError({ code: "CONFLICT", message: "E-Mail bereits vergeben" });
}
return db.users.create(input);
}),
update: protectedProcedure
.input(updateUserInput)
.output(userOutputSchema)
.mutation(async ({ input }) => {
const user = await db.users.findOne({ id: input.id });
if (!user) throw new TRPCError({ code: "NOT_FOUND" });
return db.users.update(input.id, input.data);
}),
});
tRPC Client Typsicherer Frontend-Zugriff ohne Code-Generierung
// components/UserList.tsx
import { trpc } from "@/utils/trpc";
export function UserList() {
// TypeScript weiß: data entspricht paginatedUsersSchema
const { data, isLoading } = trpc.users.getAll.useQuery({
page: 1,
limit: 20,
role: "editor", // TypeScript-Fehler bei ungültigem Wert!
});
const createUser = trpc.users.create.useMutation({
onSuccess(newUser) {
// newUser ist vollständig typisiert als UserOutputSchema
console.log(`Benutzer ${newUser.name} erstellt`);
},
});
if (isLoading) return <Spinner />;
return (
<div>
{data?.users.map((user) => (
<div key={user.id}>
<p>{user.name} ({user.role})</p>
<p>{user.email}</p>
</div>
))}
<button onClick={() => createUser.mutate({
name: "Neue Person",
email: "neu@example.com",
// TypeScript-Fehler wenn Pflichtfelder fehlen!
})}>
User erstellen
</button>
</div>
);
}
// Wenn das Backend-Schema sich ändert (z.B. neues Pflichtfeld "department"):
// → TypeScript-Fehler in ALLEN Client-Aufrufen ohne "department"
// → Kein manuelles Suchen — Compiler zeigt alle betroffenen Stellen sofort
Output-Schemas in tRPC: Das .output()-Schema validiert auch die Backend-Responses zur Laufzeit. Das schützt vor Fehlern, bei denen die Datenbank unerwartet andere Daten zurückgibt als das Schema erwartet — besonders wertvoll wenn mehrere Teams am selben Backend arbeiten.
Performance-Hinweis: Output-Validierung mit .output() hat einen kleinen Overhead. In Performance-kritischen Endpunkten mit vielen großen Objekten kann es sinnvoll sein, Output-Schemas nur in Entwicklung/Testing zu aktivieren. tRPC bietet dafür konfigurierbare Optionen per Middleware.
Fazit: Zod als Fundament für typsichere Anwendungen
Zod ist mehr als eine Validierungsbibliothek — es ist das Bindeglied zwischen TypeScript-Typen und Laufzeit-Realität. Einmal definierte Schemas gelten für Frontend-Formulare, API-Middleware und Backend-Logik gleichermaßen. Wenn sich Anforderungen ändern, reicht eine Anpassung am Schema, und TypeScript zeigt alle betroffenen Stellen im Code.
Mit Claude Code wird das Schema-Design noch schneller: Beschreibe dein Datenmodell in natürlicher Sprache, und Claude generiert produktionsreife Zod-Schemas mit korrekter Type Inference, sinnvollen Validierungsgrenzen und idiomatischen Patterns. Von einfachen CRUD-Schemas bis zu komplexen discriminatedUnion-Typen für Event-driven Architekturen.
- Single Source of Truth: Ein Schema für Typ-Definition, Frontend-Validierung und Backend-Validierung
- Null Code-Duplizierung:
z.infer<> leitet TypeScript-Typen automatisch ab
- Echte Laufzeit-Sicherheit: Typen gelten nicht nur zur Compile-Zeit, sondern schützen auch zur Laufzeit
- Composability: Schemas kombinieren, extenden, partialisieren und transformieren ohne Redundanz
- End-to-End mit tRPC: Frontend kennt Backend-Typen ohne OpenAPI-Spec oder Code-Generierung
- Custom Validators: .refine() und .superRefine() für beliebig komplexe Business-Logik
Validierungs-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges Zod-Modul mit Schema-Design, komplexen Validierungen, Custom Validators, API-Middleware und React Hook Form Integration — inkl. tRPC End-to-End Type Safety.
14 Tage kostenlos testen →