Zum Inhalt springen
EN DE

Challenge 4.4: Message Validation

Kannst Du der Nachricht vertrauen, die Dein Frontend ans Backend schickt?

Frontend Message wird validiert, bei valid zu generateText und Save to DB, bei invalid Error Response

Bevor eine Nachricht ans LLM geschickt oder in der Datenbank gespeichert wird, muss sie validiert werden. Ein Zod Schema definiert die erwartete Struktur. Ungueltige Messages werden abgefangen, bevor sie Schaden anrichten.

Ohne Validation: Dein Backend akzeptiert alles — fehlende role-Felder, leere content-Strings, unerwartete Typen, Injection-Versuche. Das fuehrt zu kryptischen Fehlern im LLM-Call, kaputten Daten in der Datenbank und potentiellen Sicherheitsluecken.

Mit Validation: Jede Message wird gegen ein Schema geprueft, bevor sie verarbeitet wird. Fehler werden frueh erkannt, mit klaren Fehlermeldungen. Deine Datenbank bleibt sauber, Dein LLM bekommt konsistente Inputs.

Zod definiert die erwartete Struktur einer Message. Du kennst Zod bereits aus Level 3 (Tool Calling) — falls noch nicht installiert: npm install zod.

import { z } from 'zod';
// Schema für eine einzelne Message
const messageSchema = z.object({
role: z.enum(['user', 'assistant', 'system']), // ← Nur erlaubte Rollen
content: z.string().min(1).max(10000), // ← Nicht leer, nicht zu lang
});
// Schema für einen Chat-Request
const chatRequestSchema = z.object({
chatId: z.string().uuid(), // ← Muss gueltige UUID sein
messages: z.array(messageSchema).min(1).max(100), // ← Mind. 1, max. 100 Messages
});

Das Schema ist die zentrale Wahrheitsquelle: Es definiert, was eine gueltige Message ist. Alles was nicht passt, wird abgelehnt.

Zod bietet zwei Wege zur Validation:

// Option 1: parse — wirft Error bei ungueltigem Input
try {
const validMessage = messageSchema.parse({
role: 'user',
content: 'Was ist TypeScript?',
});
console.log('Valid:', validMessage);
} catch (error) {
console.error('Invalid:', error);
}
// Option 2: safeParse — gibt Result-Objekt zurück (kein throw)
const result = messageSchema.safeParse({
role: 'unknown', // ← Ungueltige Rolle
content: '', // ← Leerer Content
});
if (result.success) {
console.log('Valid:', result.data);
} else {
console.log('Errors:', result.error.issues); // ← Detaillierte Fehler
// → [
// { path: ['role'], message: "Invalid enum value..." },
// { path: ['content'], message: "String must contain at least 1 character(s)" }
// ]
}

safeParse ist für API-Handler die bessere Wahl: Du bekommst strukturierte Fehler zurück und kannst dem Client eine sinnvolle Fehlermeldung schicken, statt einen unbehandelten Error zu werfen.

Baue eine Validation-Funktion, die saubere Error Responses zurueckgibt:

function validateChatRequest(body: unknown): {
success: boolean;
data?: z.infer<typeof chatRequestSchema>;
error?: string;
} {
const result = chatRequestSchema.safeParse(body);
if (result.success) {
return { success: true, data: result.data };
}
// Fehler in lesbaren String umwandeln
const errorMessages = result.error.issues
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
.join('; ');
return { success: false, error: errorMessages };
}
// Test mit gueltigen Daten
const valid = validateChatRequest({
chatId: crypto.randomUUID(),
messages: [{ role: 'user', content: 'Hallo' }],
});
console.log(valid);
// → { success: true, data: { chatId: "...", messages: [...] } }
// Test mit ungueltigen Daten
const invalid = validateChatRequest({
chatId: 'nicht-eine-uuid',
messages: [],
});
console.log(invalid);
// → { success: false, error: "chatId: Invalid uuid; messages: Array must contain at least 1 element(s)" }

Schicht 4: Injection-Schutz — Was NICHT funktioniert

Abschnitt betitelt „Schicht 4: Injection-Schutz — Was NICHT funktioniert“

Achtung: Das folgende Beispiel zeigt einen verbreiteten Fehler. String-basierte Blocklisten sind kein wirksamer Injection-Schutz — sie lassen sich trivial umgehen (Tippfehler, Unicode, Umschreibungen). Wir zeigen es hier, damit Du dieses Anti-Pattern erkennst.

// ❌ ANTI-PATTERN: String-Matching für Injection-Schutz
const naiveSchema = z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z
.string()
.min(1)
.max(10000)
.refine(
(content) => !content.includes('ignore previous instructions'),
{ message: 'Potentially malicious content detected' },
),
});
// Leicht zu umgehen:
// "1gnore previous instruct1ons" → nicht erkannt
// "Vergiss alles davor" → nicht erkannt

Was stattdessen hilft:

  • Strukturelle Validation (Schicht 1-3 oben): Laenge, Typ, Format prüfen
  • Content Moderation APIs (z.B. OpenAI Moderation, Anthropic Content Filtering): ML-basierte Erkennung
  • Output-Validation: Pruefe die LLM-Antwort, nicht nur den Input
  • Rate Limiting: Begrenze Anfragen pro User/IP
  • Least Privilege: Gib dem LLM nur die Tools und Daten, die es braucht

Das Prinzip bleibt: Validate first, process second — aber mit den richtigen Mitteln.

So sieht Validation in einem API-Handler aus:

async function handleChatRequest(req: Request): Promise<Response> {
const body = await req.json();
// 1. Validate
const validation = validateChatRequest(body);
if (!validation.success) {
return new Response(
JSON.stringify({ error: validation.error }),
{ status: 400 }, // ← 400 Bad Request
);
}
// 2. Ab hier: data ist typsicher
const { chatId, messages } = validation.data!;
// 3. Weiter mit generateText, Persistence etc.
console.log(`Valid request: Chat ${chatId}, ${messages.length} messages`);
return new Response(JSON.stringify({ status: 'ok' }));
}

Der Typ von validation.data ist automatisch aus dem Zod Schema abgeleitet — volle TypeScript Type Safety ohne manuelles Typing.

Aufgabe: Definiere ein Zod Message-Schema, validiere eingehende Messages und handle ungueltige Messages graceful.

Erstelle eine Datei challenge-4-4.ts:

import { z } from 'zod';
// TODO 1: Definiere messageSchema mit:
// - role: enum ['user', 'assistant', 'system']
// - content: string, min 1, max 5000
// TODO 2: Definiere chatRequestSchema mit:
// - chatId: string, uuid
// - messages: array von messageSchema, min 1, max 50
// TODO 3: Schreibe eine validate-Funktion die safeParse nutzt und
// bei Fehler einen lesbaren Error-String zurueckgibt
// TODO 4: Teste mit diesen Inputs:
const testCases = [
// Gueltig
{ chatId: crypto.randomUUID(), messages: [{ role: 'user', content: 'Hallo' }] },
// Ungueltige Rolle
{ chatId: crypto.randomUUID(), messages: [{ role: 'admin', content: 'Hallo' }] },
// Leerer Content
{ chatId: crypto.randomUUID(), messages: [{ role: 'user', content: '' }] },
// Keine UUID
{ chatId: '123', messages: [{ role: 'user', content: 'Hallo' }] },
// Leere Messages
{ chatId: crypto.randomUUID(), messages: [] },
];

Checkliste:

  • messageSchema definiert mit role und content
  • chatRequestSchema definiert mit chatId und messages
  • validate-Funktion nutzt safeParse
  • Gueltiger Input gibt { success: true, data: ... } zurück
  • Ungueltiger Input gibt lesbaren Error-String zurück
  • Alle 5 Test Cases liefern erwartete Ergebnisse

Ausfuehren mit: npx tsx challenge-4-4.ts

Lösung anzeigen
import { z } from 'zod';
const messageSchema = z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string().min(1, 'Content darf nicht leer sein').max(5000),
});
const chatRequestSchema = z.object({
chatId: z.string().uuid('chatId muss eine gueltige UUID sein'),
messages: z
.array(messageSchema)
.min(1, 'Mindestens eine Message erforderlich')
.max(50),
});
function validate(body: unknown): {
success: boolean;
data?: z.infer<typeof chatRequestSchema>;
error?: string;
} {
const result = chatRequestSchema.safeParse(body);
if (result.success) {
return { success: true, data: result.data };
}
const errorMessages = result.error.issues
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
.join('; ');
return { success: false, error: errorMessages };
}
// Tests
const testCases = [
{ chatId: crypto.randomUUID(), messages: [{ role: 'user', content: 'Hallo' }] },
{ chatId: crypto.randomUUID(), messages: [{ role: 'admin', content: 'Hallo' }] },
{ chatId: crypto.randomUUID(), messages: [{ role: 'user', content: '' }] },
{ chatId: '123', messages: [{ role: 'user', content: 'Hallo' }] },
{ chatId: crypto.randomUUID(), messages: [] },
];
testCases.forEach((tc, i) => {
const result = validate(tc);
console.log(
`Test ${i + 1}: ${result.success ? 'VALID' : `INVALID → ${result.error}`}`,
);
});
// Test 1: VALID
// Test 2: INVALID → messages.0.role: Invalid enum value...
// Test 3: INVALID → messages.0.content: Content darf nicht leer sein
// Test 4: INVALID → chatId: chatId muss eine gueltige UUID sein
// Test 5: INVALID → messages: Mindestens eine Message erforderlich

Erklärung: safeParse gibt ein Result-Objekt zurück statt einen Error zu werfen. Die error.issues enthalten den Pfad zum fehlerhaften Feld und eine Beschreibung. Die Custom Error Messages (z.B. “Content darf nicht leer sein”) machen die Fehlermeldungen für Entwickler und User verständlich.

Frontend wird validiert, bei valid loadMessages, generateText, onFinish, saveMessages; bei invalid 400 Error

Uebung: Integriere Validation in den Persistence-Flow — validiere Messages BEVOR sie in die Datenbank gespeichert werden.

  1. Nutze den chatRequestSchema aus der TRY-Uebung
  2. Baue eine handleChat-Funktion die: a) Den Request-Body validiert b) Bei Fehler: Error-Response zurueckgibt (kein DB-Zugriff, kein LLM-Call) c) Bei Erfolg: Verlauf laden, generateText aufrufen, im onFinish speichern
  3. Teste mit gueltigen und ungueltigen Requests
  4. Stelle sicher: Ungueltige Messages landen NICHT in der Datenbank

Optional Stretch Goal: Erweitere das Schema um ein metadata-Feld (optional) mit timestamp und source (z.B. “web”, “api”, “mobile”). Validiere, dass timestamp ein gueltiges ISO-Datum ist.

Part of AI Learning — free courses from prompt to production. Jan on LinkedIn