Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
# Zod Schema Validation: Type-Safe Validation für TypeScript
|
||||
|
||||
**Meta-Description:** Umfassender Guide zu Zod für Schema-Validierung. TypeScript-Integration, API-Validation, Form-Handling und Best Practices für robuste Anwendungen.
|
||||
|
||||
**Keywords:** Zod, Schema Validation, TypeScript Validation, Form Validation, API Validation, Runtime Validation, Type Safety
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Zod ist die führende Schema-Validierungsbibliothek für TypeScript. Sie bietet **Runtime-Validierung mit automatischer Type-Inferenz** – eine perfekte Brücke zwischen TypeScript's Compile-Time-Checks und der Realität von externen Daten.
|
||||
|
||||
---
|
||||
|
||||
## Das Problem
|
||||
|
||||
```typescript
|
||||
// TypeScript prüft nur zur Compile-Time
|
||||
interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
// Aber was passiert zur Runtime?
|
||||
const userData = await fetch('/api/user').then(r => r.json());
|
||||
|
||||
// userData könnte ALLES sein - TypeScript vertraut blind
|
||||
const user: User = userData; // Keine Runtime-Prüfung!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Die Lösung: Zod
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema definieren
|
||||
const UserSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive()
|
||||
});
|
||||
|
||||
// Type automatisch inferieren
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
// { name: string; email: string; age: number }
|
||||
|
||||
// Runtime-Validierung
|
||||
const userData = await fetch('/api/user').then(r => r.json());
|
||||
const user = UserSchema.parse(userData); // Wirft bei ungültigen Daten!
|
||||
|
||||
// Oder mit safeParse für Error-Handling
|
||||
const result = UserSchema.safeParse(userData);
|
||||
if (result.success) {
|
||||
console.log(result.data); // Typisiert als User
|
||||
} else {
|
||||
console.error(result.error.errors);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basis-Typen
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Primitive Typen
|
||||
const stringSchema = z.string();
|
||||
const numberSchema = z.number();
|
||||
const booleanSchema = z.boolean();
|
||||
const dateSchema = z.date();
|
||||
const bigintSchema = z.bigint();
|
||||
const symbolSchema = z.symbol();
|
||||
const undefinedSchema = z.undefined();
|
||||
const nullSchema = z.null();
|
||||
const voidSchema = z.void();
|
||||
const anySchema = z.any();
|
||||
const unknownSchema = z.unknown();
|
||||
const neverSchema = z.never();
|
||||
|
||||
// Literale
|
||||
const tuna = z.literal('tuna');
|
||||
const twelve = z.literal(12);
|
||||
const isTrue = z.literal(true);
|
||||
|
||||
// Enums
|
||||
const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']);
|
||||
type FishEnum = z.infer<typeof FishEnum>; // 'Salmon' | 'Tuna' | 'Trout'
|
||||
|
||||
// Native Enums
|
||||
enum Fruits {
|
||||
Apple,
|
||||
Banana
|
||||
}
|
||||
const FruitEnum = z.nativeEnum(Fruits);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String Validierung
|
||||
|
||||
```typescript
|
||||
const stringSchema = z.string()
|
||||
// Länge
|
||||
.min(5, 'Mindestens 5 Zeichen')
|
||||
.max(100, 'Maximal 100 Zeichen')
|
||||
.length(10, 'Exakt 10 Zeichen')
|
||||
|
||||
// Format
|
||||
.email('Ungültige E-Mail')
|
||||
.url('Ungültige URL')
|
||||
.uuid('Ungültige UUID')
|
||||
.cuid('Ungültige CUID')
|
||||
.datetime('Ungültiges Datum')
|
||||
.ip('Ungültige IP')
|
||||
|
||||
// Regex
|
||||
.regex(/^[a-z]+$/, 'Nur Kleinbuchstaben')
|
||||
|
||||
// Transformationen
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.toUpperCase()
|
||||
|
||||
// Custom
|
||||
.refine(val => val.includes('@'), 'Muss @ enthalten');
|
||||
|
||||
// Praktisches Beispiel
|
||||
const UsernameSchema = z.string()
|
||||
.min(3, 'Username zu kurz')
|
||||
.max(20, 'Username zu lang')
|
||||
.regex(/^[a-zA-Z0-9_]+$/, 'Nur Buchstaben, Zahlen und _')
|
||||
.toLowerCase();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Number Validierung
|
||||
|
||||
```typescript
|
||||
const numberSchema = z.number()
|
||||
// Constraints
|
||||
.gt(0, 'Größer als 0')
|
||||
.gte(0, 'Größer oder gleich 0')
|
||||
.lt(100, 'Kleiner als 100')
|
||||
.lte(100, 'Kleiner oder gleich 100')
|
||||
.positive('Muss positiv sein')
|
||||
.negative('Muss negativ sein')
|
||||
.nonpositive()
|
||||
.nonnegative()
|
||||
.multipleOf(5, 'Muss durch 5 teilbar sein')
|
||||
.int('Muss Ganzzahl sein')
|
||||
.finite()
|
||||
.safe(); // JavaScript safe integer
|
||||
|
||||
// Praktisches Beispiel: Preis
|
||||
const PriceSchema = z.number()
|
||||
.positive('Preis muss positiv sein')
|
||||
.multipleOf(0.01, 'Maximal 2 Dezimalstellen')
|
||||
.max(1000000, 'Preis zu hoch');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Object Schemas
|
||||
|
||||
```typescript
|
||||
// Basis Object
|
||||
const UserSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive()
|
||||
});
|
||||
|
||||
// Optional & Default
|
||||
const UserWithDefaultsSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
age: z.number().optional(), // number | undefined
|
||||
role: z.string().default('user'), // Hat immer einen Wert
|
||||
isActive: z.boolean().nullable() // boolean | null
|
||||
});
|
||||
|
||||
// Extend
|
||||
const AdminSchema = UserSchema.extend({
|
||||
permissions: z.array(z.string())
|
||||
});
|
||||
|
||||
// Pick & Omit
|
||||
const UserNameOnly = UserSchema.pick({ name: true });
|
||||
const UserWithoutAge = UserSchema.omit({ age: true });
|
||||
|
||||
// Partial & Required
|
||||
const PartialUser = UserSchema.partial(); // Alle optional
|
||||
const RequiredUser = PartialUser.required(); // Alle required
|
||||
|
||||
// Strict Mode
|
||||
const StrictUser = UserSchema.strict(); // Wirft bei extra Keys
|
||||
|
||||
// Passthrough & Strip
|
||||
const PassthroughUser = UserSchema.passthrough(); // Behält extra Keys
|
||||
const StrippedUser = UserSchema.strip(); // Entfernt extra Keys (default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Array & Tuple
|
||||
|
||||
```typescript
|
||||
// Array
|
||||
const StringArraySchema = z.array(z.string());
|
||||
const NumberArraySchema = z.array(z.number())
|
||||
.min(1, 'Mindestens 1 Element')
|
||||
.max(10, 'Maximal 10 Elemente')
|
||||
.nonempty('Darf nicht leer sein');
|
||||
|
||||
// Tuple
|
||||
const CoordinateSchema = z.tuple([
|
||||
z.number(), // x
|
||||
z.number(), // y
|
||||
z.number().optional() // z (optional)
|
||||
]);
|
||||
|
||||
type Coordinate = z.infer<typeof CoordinateSchema>;
|
||||
// [number, number, number?]
|
||||
|
||||
// Rest Elements
|
||||
const StringsThenNumbers = z.tuple([z.string(), z.string()])
|
||||
.rest(z.number());
|
||||
// [string, string, ...number[]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Union & Discriminated Union
|
||||
|
||||
```typescript
|
||||
// Union
|
||||
const StringOrNumber = z.union([z.string(), z.number()]);
|
||||
|
||||
// Shorthand
|
||||
const StringOrNull = z.string().nullable(); // string | null
|
||||
const StringOrUndefined = z.string().optional(); // string | undefined
|
||||
|
||||
// Discriminated Union (performanter)
|
||||
const ResultSchema = z.discriminatedUnion('status', [
|
||||
z.object({ status: z.literal('success'), data: z.string() }),
|
||||
z.object({ status: z.literal('error'), error: z.string() })
|
||||
]);
|
||||
|
||||
type Result = z.infer<typeof ResultSchema>;
|
||||
// { status: 'success'; data: string } | { status: 'error'; error: string }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transformationen
|
||||
|
||||
```typescript
|
||||
// Transform Output
|
||||
const NumberFromString = z.string().transform(val => parseInt(val, 10));
|
||||
// Input: string, Output: number
|
||||
|
||||
// Preprocessing
|
||||
const NumberSchema = z.preprocess(
|
||||
(val) => {
|
||||
if (typeof val === 'string') return parseInt(val, 10);
|
||||
return val;
|
||||
},
|
||||
z.number()
|
||||
);
|
||||
|
||||
// Praktisches Beispiel: API Response
|
||||
const ApiResponseSchema = z.object({
|
||||
created_at: z.string().transform(val => new Date(val)),
|
||||
price_cents: z.number().transform(val => val / 100),
|
||||
is_active: z.union([z.boolean(), z.literal('true'), z.literal('false')])
|
||||
.transform(val => val === true || val === 'true')
|
||||
});
|
||||
|
||||
// Input: { created_at: "2024-01-15", price_cents: 1999, is_active: "true" }
|
||||
// Output: { created_at: Date, price_cents: 19.99, is_active: true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Validation mit Refine
|
||||
|
||||
```typescript
|
||||
// Einfaches Refine
|
||||
const PasswordSchema = z.string()
|
||||
.min(8)
|
||||
.refine(
|
||||
(val) => /[A-Z]/.test(val),
|
||||
'Muss Großbuchstaben enthalten'
|
||||
)
|
||||
.refine(
|
||||
(val) => /[0-9]/.test(val),
|
||||
'Muss Zahlen enthalten'
|
||||
);
|
||||
|
||||
// Superrefine für komplexe Logik
|
||||
const SignupSchema = z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string()
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.password !== data.confirmPassword) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Passwörter stimmen nicht überein',
|
||||
path: ['confirmPassword']
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Async Validation
|
||||
const UniqueEmailSchema = z.string().email().refine(
|
||||
async (email) => {
|
||||
const exists = await checkEmailExists(email);
|
||||
return !exists;
|
||||
},
|
||||
'E-Mail bereits vergeben'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration mit React Hook Form
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const SignupSchema = z.object({
|
||||
name: z.string().min(2, 'Name zu kurz'),
|
||||
email: z.string().email('Ungültige E-Mail'),
|
||||
password: z.string().min(8, 'Mindestens 8 Zeichen')
|
||||
});
|
||||
|
||||
type SignupData = z.infer<typeof SignupSchema>;
|
||||
|
||||
function SignupForm() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<SignupData>({
|
||||
resolver: zodResolver(SignupSchema)
|
||||
});
|
||||
|
||||
const onSubmit = (data: SignupData) => {
|
||||
console.log(data); // Typsicher!
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input {...register('name')} />
|
||||
{errors.name && <span>{errors.name.message}</span>}
|
||||
|
||||
<input {...register('email')} />
|
||||
{errors.email && <span>{errors.email.message}</span>}
|
||||
|
||||
<input type="password" {...register('password')} />
|
||||
{errors.password && <span>{errors.password.message}</span>}
|
||||
|
||||
<button type="submit">Registrieren</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Route Validation (Next.js)
|
||||
|
||||
```typescript
|
||||
// app/api/users/route.ts
|
||||
import { z } from 'zod';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
role: z.enum(['admin', 'user']).default('user')
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const data = CreateUserSchema.parse(body);
|
||||
|
||||
// data ist typsicher
|
||||
const user = await createUser(data);
|
||||
|
||||
return NextResponse.json(user, { status: 201 });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ errors: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
try {
|
||||
UserSchema.parse({ name: 'A', email: 'invalid' });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// Formatierte Errors
|
||||
console.log(error.format());
|
||||
/*
|
||||
{
|
||||
name: { _errors: ['String must contain at least 2 character(s)'] },
|
||||
email: { _errors: ['Invalid email'] }
|
||||
}
|
||||
*/
|
||||
|
||||
// Flache Error-Liste
|
||||
console.log(error.flatten());
|
||||
/*
|
||||
{
|
||||
formErrors: [],
|
||||
fieldErrors: {
|
||||
name: ['String must contain at least 2 character(s)'],
|
||||
email: ['Invalid email']
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Zod bietet:
|
||||
|
||||
1. **Runtime + Compile-Time Safety**: Validierung wo TypeScript aufhört
|
||||
2. **Type Inference**: Keine doppelten Definitionen
|
||||
3. **Composability**: Schemas kombinieren und erweitern
|
||||
4. **Framework-Agnostisch**: React, Vue, Node.js, etc.
|
||||
|
||||
Für jede Anwendung mit externen Daten ist Zod unverzichtbar.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Shield protecting data, validation concept, type-safe illustration"
|
||||
2. "TypeScript and runtime validation merging, code security concept"
|
||||
3. "Form with checkmarks appearing as user types, validation feedback visualization"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Zod Documentation](https://zod.dev/)
|
||||
- [Zod GitHub](https://github.com/colinhacks/zod)
|
||||
- [React Hook Form + Zod](https://react-hook-form.com/get-started#SchemaValidation)
|
||||
- [tRPC + Zod Integration](https://trpc.io/docs/server/validators)
|
||||
Reference in New Issue
Block a user