2026

1. April 2026

Server-Driven Forms: Validierungslogik nicht duplizieren

Wenn das Frontend Backend-Geschaeftsregeln fuer die Formularvalidierung nachbaut, entsteht ein Wartungsproblem. Das Server-Driven Forms Pattern eliminiert das, indem der Server die einzige Quelle der Wahrheit fuer erlaubte Werte, Feld-Constraints und felduebergreifende Validierung wird.

S
Sascha Becker
Author

19 Min. Lesezeit

Server-Driven Forms: Validierungslogik nicht duplizieren

Server-Driven Forms: Validierungslogik nicht duplizieren

Es gibt ein Problem, das vermutlich die meisten Frontend-Entwickler kennen. Du hast einen REST-Endpoint (oder eine GraphQL-Mutation), der ein komplexes Objekt entgegennimmt. Das Objekt hat konditionale Constraints: Wenn ein Attribut sich aendert, muss ein anderes anders sein. Manche Felder erlauben nur bestimmte Werte abhaengig vom aktuellen Zustand anderer Felder. Die API gibt einen fehlerhaften HTTP-Response zurueck, wenn das Objekt nicht stimmt.

Der Instinkt des Frontend-Teams ist, all diese Logik in React nachzubauen. Pflichtfelder, konditionale Anforderungen, dynamische Dropdown-Optionen, felduebergreifende Regeln. Das Ergebnis waren hunderte Zeilen Validierungscode, der das Backend spiegelte, mit der Zeit auseinanderging und auf Weisen brach, die erst auffielen, wenn der Server ablehnte, was der Client durchgewunken hatte.

Das ist ein bekannter architektonischer Code Smell. Die Industrie hat einen Namen fuer die Loesung: Server-Driven Forms.

Dieser Artikel geht das Pattern anhand eines konkreten Beispiels durch: ein Zoo-Tiertransfer-Formular. Nicht weil du Zoo-Software baust, sondern weil es die Art von Formular ist, bei der die Komplexitaet sofort offensichtlich wird und clientseitige Validierung schnell zusammenbricht.

Das Beispiel: Zoo-Tiertransfer

Stell dir vor, du baust das interne Tool fuer ein Zoo-Netzwerk, das Tiertransfers zwischen Einrichtungen verwaltet. Ein Tierpfleger muss einen Transferantrag ausfuellen. Das Formular sieht auf den ersten Blick einfach aus: Tier auswaehlen, Zielort auswaehlen, Transportmethode waehlen, Datum setzen.

Aber die Daten kommen von ueberall:

  • Das Artenregister bestimmt den CITES-Schutzstatus und gesetzliche Anforderungen
  • Die Einrichtungsdatenbank weiss, welche Gehege verfuegbar sind und welche Habitate sie unterstuetzen
  • Das Veterinaersystem verfolgt Gesundheitszertifikate und deren Ablaufdaten
  • Die Regulierungs-API liefert Import-/Exportregeln pro Land
  • Das Transportsystem kennt Containeranforderungen und Carrier-Verfuegbarkeit pro Art

Und die Regeln kaskadieren. Die Art zu aendern veraendert, welche Zielorte geeignete Gehege haben. Das Zielland zu aendern veraendert, welche Gesundheitszertifikate erforderlich sind und wie aktuell sie sein muessen. Luftfracht fuer einen Primaten loest eine Container-Belueftungsanforderung aus, die fuer Reptilien nicht gilt. Eine CITES-Anhang-I-Art erfordert eine ministerielle Genehmigung, was Felder hinzufuegt, die fuer Anhang-II-Tiere nicht existieren.

Versuch mal, all das in React zu duplizieren.

Warum clientseitige Validierung scheitert

Wenn du versuchst, diese Regeln im Client abzubilden, landest du bei so etwas:

tsx
function TransferForm({ transfer }: { transfer: Transfer }) {
const [values, setValues] = useState(transfer);
// dupliziert vom Backend
const isCitesI = getCitesStatus(values.speciesId) === "APPENDIX_I";
const needsMinisterialApproval = isCitesI;
const allowedDestinations = getDestinationsWithEnclosure(values.speciesId);
const requiredCerts = getRequiredCerts(values.speciesId, values.destinationId);
const allowedTransport = getTransportMethods(values.speciesId, values.weight);
return (
<form>
<Select name="species" options={allSpecies} />
<Select name="destination" options={allowedDestinations} />
<Select name="transport" options={allowedTransport} />
{needsMinisterialApproval && <TextField name="approvalReference" />}
{/* ... 15 weitere konditionale Felder */}
</form>
);
}

Jede dieser Hilfsfunktionen ist eine Kopie von Server-Logik. getCitesStatus dupliziert das Artenregister. getDestinationsWithEnclosure dupliziert einen Join ueber die Einrichtungsdatenbank. getRequiredCerts dupliziert Regulierungsregeln, die sich aendern, wenn die Gesetzgebung aktualisiert wird.

Wenn das Regulierungsteam eine neue Quarantaeneregel fuer Amphibien hinzufuegt, wird das Backend aktualisiert. Das Frontend erfaehrt davon wochenlang nichts. In der Zwischenzeit schickt ein Tierpfleger einen Transfer ab, der die Client-Validierung besteht, aber auf dem Server mit einem rohen 400-Fehler scheitert, der nichts Nuetzliches sagt.

Das tiefere Problem ist architektonisch: Geschaeftsregeln leben an zwei Stellen.

Moment. Sollte dieses Formular ueberhaupt so existieren?

Bevor du zu Server-Driven Forms greifst, stell eine haertere Frage: Warum hat dieses Formular ueberhaupt so viele felduebergreifende Abhaengigkeiten?

Ein Formular, bei dem das Aendern eines Dropdowns fuenf andere Felder umstrukturiert, ist nicht nur ein Validierungsproblem. Es ist ein UX-Problem. Der Nutzer wird gebeten, zu viele voneinander abhaengige Entscheidungen auf einem einzigen Screen zu treffen. Ein mehrstufiger Wizard, der Entscheidungen in sequenzielle Schritte aufteilt, eliminiert den Grossteil der kaskadierenden Komplexitaet:

  1. Schritt 1: Art auswaehlen. Festlegen. Weiter.
  2. Schritt 2: Zielort auswaehlen. Der Server kennt die Art bereits, also zeigt er nur gueltige Zielorte. Kein Kaskadieren. Keine deaktivierten Dropdowns. Kein "Zuerst eine Art auswaehlen"-Hilfstext.
  3. Schritt 3: Transport und Daten. Die Constraints sind jetzt auf das festgelegte Art-Zielort-Paar beschraenkt.

Jeder Schritt ist ein einfaches Formular ohne felduebergreifende Abhaengigkeiten. Jeder Schritt validiert isoliert. Die "wenn A sich aendert, muss B anders sein"-Regeln verschwinden, weil A bereits feststeht, bevor B erscheint.

Server-Driven Forms loesen das "wir haben dieses komplexe Formular und die Validierungslogik ist dupliziert"-Problem. Gutes UX-Design verhindert, dass das Problem ueberhaupt entsteht. Wenn du dein Formular in Schritte aufteilen kannst, mach das zuerst. Wenn du es versucht hast und die Komplexitaet wirklich nicht reduzierbar ist (der Nutzer muss mehrere zusammenhaengende Felder gleichzeitig sehen und anpassen, das Formular kann nicht sequenziell sein, oder du erbst ein Formular, das bereits so gebaut ist), dann lies weiter.

Das Prinzip: Server als Single Source of Truth

Wenn du entschieden hast, dass die Formularkomplexitaet real und unvermeidbar ist, ist die Loesung nicht bessere clientseitige Validierung. Die Loesung ist, clientseitige Geschaeftslogik komplett zu entfernen. Das Frontend sollte nur zwei Dinge tun:

  1. UX-Validierung: "Dieses Feld ist erforderlich", "Das muss eine Zahl sein", "Mindestens 3 Zeichen". Dinge, die die Benutzererfahrung verbessern, aber keine geschaeftliche Bedeutung haben.
  2. Rendering: Anzeigen, was der Server sagt. Fehler, erlaubte Werte, deaktivierte Zustaende, Constraint-Meldungen.

Alles andere kommt vom Server. Welche Zielorte geeignete Gehege fuer diese Art haben. Welche Transportmethoden fuer dieses Tier legal sind. Ob das aktuelle Gesundheitszertifikat noch aktuell genug ist. Der Server beantwortet all diese Fragen, weil der Server bereits Zugriff auf alle Datenquellen hat.

Das Pattern: Server-generiertes JSON Schema

Statt ein eigenes Response-Format zu erfinden, spricht der Server eine Sprache, die ein existierender Form-Renderer bereits versteht: JSON Schema. Der Server generiert bei jedem Request dynamisch ein Schema, und der Client reicht es direkt an den Renderer weiter. Kein Custom-Mapping-Code, keine handgeschriebenen Feld-Komponenten.

Der Server gibt drei Objekte zurueck:

  1. schema (JSON Schema): welche Felder existieren, ihre Typen, erlaubte Werte (oneOf), welche erforderlich sind
  2. uiSchema: welche Felder deaktiviert, versteckt oder mit Hilfstext versehen sind
  3. extraErrors: feldbezogene Validierungsfehler vom Server

Der Client nutzt react-jsonschema-form (RJSF) mit dem MUI-Theme, um alle drei zu rendern. Das Frontend wird zum Durchreicher.

Initialer Load (neuen Transfer erstellen)

Das Formular mounted und sendet den leeren Zustand an POST /transfer/form-state. Der Server antwortet:

json
{
"schema": {
"type": "object",
"required": ["speciesId"],
"properties": {
"speciesId": {
"type": "string",
"title": "Art",
"oneOf": [
{ "const": "PAN_TROG", "title": "Schimpanse" },
{ "const": "CROC_NIL", "title": "Nilkrokodil" },
{ "const": "DENDRO_AZU", "title": "Blauer Pfeilgiftfrosch" }
]
},
"destinationId": {
"type": "string",
"title": "Zielort",
"oneOf": []
},
"transportMethod": {
"type": "string",
"title": "Transportmethode",
"oneOf": []
}
}
},
"uiSchema": {
"destinationId": {
"ui:disabled": true,
"ui:help": "Zuerst eine Art auswaehlen"
},
"transportMethod": {
"ui:disabled": true,
"ui:help": "Zuerst Art und Zielort auswaehlen"
}
},
"extraErrors": {}
}

RJSF rendert ein aktiviertes Art-Dropdown und deaktivierte Zielort-/Transport-Dropdowns mit Hilfstext. Das Frontend hat null Meinung dazu, warum der Zielort deaktiviert ist. Es rendert einfach, was der Server gesagt hat.

Nach Auswahl von "Schimpanse"

Das Formular sendet den aktualisierten Zustand. Der Server berechnet alles neu und gibt ein neues Schema zurueck:

json
{
"schema": {
"type": "object",
"required": ["speciesId", "destinationId", "ministerialApprovalRef"],
"properties": {
"speciesId": {
"type": "string",
"title": "Art",
"oneOf": [
{ "const": "PAN_TROG", "title": "Schimpanse" },
{ "const": "CROC_NIL", "title": "Nilkrokodil" },
{ "const": "DENDRO_AZU", "title": "Blauer Pfeilgiftfrosch" }
]
},
"destinationId": {
"type": "string",
"title": "Zielort",
"oneOf": [
{ "const": "ZOO_BER", "title": "Zoo Berlin" },
{ "const": "ZOO_VIE", "title": "Tiergarten Schoenbrunn" }
]
},
"transportMethod": {
"type": "string",
"title": "Transportmethode",
"oneOf": []
},
"ministerialApprovalRef": {
"type": "string",
"title": "Ministerielle Genehmigungsreferenz"
},
"healthCertificateDate": {
"type": "string",
"format": "date",
"title": "Datum Gesundheitszertifikat"
}
}
},
"uiSchema": {
"transportMethod": {
"ui:disabled": true,
"ui:help": "Zuerst einen Zielort auswaehlen"
},
"ministerialApprovalRef": {
"ui:help": "CITES Anhang I: Ministerielle Genehmigung erforderlich"
},
"healthCertificateDate": {
"ui:help": "Muss innerhalb von 10 Tagen vor Transport ausgestellt sein"
}
},
"extraErrors": {}
}

Die Auswahl von "Schimpanse" (eine CITES-Anhang-I-Art) hat durch das gesamte Formular kaskadiert. Zielorte wurden auf Einrichtungen mit Primatengehegen eingeschraenkt. Ein Feld fuer ministerielle Genehmigung ist im Schema mit einem required-Constraint erschienen. Ein Datumsfeld fuer das Gesundheitszertifikat ist mit Hilfstext zur Aktualitaetsregel erschienen. Das Frontend hat nichts davon berechnet. Es hat ein neues Schema empfangen und RJSF hat neu gerendert.

Validierungsfehler

Wenn der Nutzer das Formular ausfuellt, aber ein Transportdatum zu nah am Zertifikatsdatum waehlt, gibt der Server Fehler ueber extraErrors zurueck:

json
{
"schema": { "..." },
"uiSchema": { "..." },
"extraErrors": {
"healthCertificateDate": {
"__errors": [
"Gesundheitszertifikat ist am Transportdatum aelter als 10 Tage"
]
},
"ministerialApprovalRef": {
"__errors": [
"Ministerielle Genehmigungsreferenz ist fuer CITES-Anhang-I-Arten erforderlich"
]
}
}
}

RJSF rendert diese als MUI-FormHelperText-Fehlermeldungen unter jedem Feld. Das Frontend berechnet keine Zertifikats-Gueltigkeitsfenster. Es weiss nicht, was CITES Anhang I bedeutet. Der Server weiss es, und RJSF zeigt an, was er zurueckgegeben hat.

Der kaskadierende Update-Flow

Das passiert, wenn der Tierpfleger die Art mitten in der Bearbeitung von "Schimpanse" auf "Nilkrokodil" aendert:

  1. Nutzer waehlt "Nilkrokodil" im Art-Dropdown
  2. Frontend sendet die aktualisierten formData an POST /transfer/form-state
  3. Server berechnet alles neu und gibt ein neues Schema zurueck:
    • Zielorte aktualisiert (nur Einrichtungen mit Reptiliengehegen ueber neue oneOf-Werte)
    • Der zuvor ausgewaehlte "Tiergarten Schoenbrunn" ist moeglicherweise nicht mehr in der oneOf-Liste
    • Transportmethoden aendern sich (neue oneOf-Optionen)
    • ministerialApprovalRef verschwindet aus dem Schema (Nilkrokodil ist CITES Anhang II, nicht I)
    • ui:help des Gesundheitszertifikats aendert sich auf "Muss innerhalb von 21 Tagen vor Transport ausgestellt sein"
  4. Frontend uebergibt das neue Schema an RJSF, das das gesamte Formular neu rendert

Wenn der zuvor ausgewaehlte Zielort nicht mehr in der oneOf-Liste ist, zeigt RJSF ihn als ungueltig an. Der Nutzer waehlt einen neuen Zielort, was einen weiteren Round-Trip ausloest, und das Formular pendelt sich in einen gueltigen Zustand ein.

Das ist die zentrale Erkenntnis: Das Formular ist ein Gespraech mit dem Server. Jede Aenderung ist eine Frage ("Was sind meine Optionen, basierend auf dem, was ich bisher ausgewaehlt habe?"), und der Server antwortet mit einem vollstaendigen JSON Schema.

Wer macht das in Produktion?

Das ist kein theoretisches Pattern. Mehrere grosse Produkte setzen auf Server-Driven Forms:

Atlassian Jira nutzt GET /rest/api/3/issue/createmeta, um Felder, erlaubte Werte und den Required/Optional-Status fuer das Erstellen eines Issues zurueckzugeben. Wenn du den Issue-Typ aenderst, ruft das Frontend den Endpoint erneut auf, um die neue Feldkonfiguration zu holen. Jedes Dropdown, jedes Pflichtfeld, jeder Constraint kommt vom Server.

Salesforce hat ihre gesamte Plattform auf metadaten-getriebenen Formularen aufgebaut. Objekt-Schemas, Feld-Abhaengigkeiten, Validierungsregeln und Seitenlayouts sind alle server-owned. Lightning Dynamic Forms erlauben Admins, Formularlayouts deklarativ zu konfigurieren, und die Runtime holt diese Konfiguration.

Stripe nutzt teilweise server-getriebene Formulare in ihrem Payment Element. Verfuegbare Zahlungsmethoden und ihre erforderlichen Felder werden von Stripes Servern basierend auf der Haendlerkonfiguration und dem Kundenstandort abgerufen. Das Frontend rendert, was zurueckkommt.

Google Cloud APIs unterstuetzen einen validateOnly-Parameter auf Create-Methoden (dokumentiert in AIP-163), was genau dem Dry-Run-Validierungspattern entspricht.

React-Implementierung mit RJSF

Die gesamte Frontend-Implementierung besteht aus vier Paketen und einer kleinen Komponente. Keine Custom-Field-Renderer, keine Mapping-Logik.

Setup
bash
pnpm add @rjsf/core @rjsf/mui @rjsf/utils @rjsf/validator-ajv8

Das sind die einzigen Abhaengigkeiten ueber das hinaus, was du bereits hast (@mui/material, @mui/icons-material, Emotion). Alle vier Pakete teilen die gleiche Versionsnummer (aktuell v6), also immer passende Versionen installieren.

Der Form-State-Hook
tsx
import { useQuery } from "@tanstack/react-query";
import type { RJSFSchema, UiSchema, ErrorSchema } from "@rjsf/utils";
type FormStateResponse = {
schema: RJSFSchema;
uiSchema: UiSchema;
extraErrors: ErrorSchema;
};
function useTransferFormState(formData: Record<string, unknown>) {
return useQuery<FormStateResponse>({
queryKey: ["transfer-form-state", formData],
queryFn: async ({ signal }) => {
const res = await fetch("/api/transfer/form-state", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
signal,
});
return res.json();
},
staleTime: 0,
});
}
Die Form-Komponente
tsx
import { useState } from "react";
import Form from "@rjsf/mui";
import validator from "@rjsf/validator-ajv8";
import { useDebouncedValue } from "./useDebouncedValue";
function TransferForm({ initial }: { initial?: Record<string, unknown> }) {
const [formData, setFormData] = useState(initial ?? {});
const debouncedData = useDebouncedValue(formData, 300);
const { data: formState } = useTransferFormState(debouncedData);
if (!formState) return null;
return (
<Form
schema={formState.schema}
uiSchema={formState.uiSchema}
extraErrors={formState.extraErrors}
formData={formData}
validator={validator}
noValidate
onChange={({ formData }) => setFormData(formData)}
onSubmit={({ formData }) => submitTransfer(formData)}
/>
);
}

Das ist alles. Die gesamte Form-Komponente hat 20 Zeilen. Kein if (species === "PAN_TROG") irgendwo. Kein getCitesStatus. Keine Custom-<Select>-Wrapper. RJSF liest das JSON Schema und rendert MUI-Komponenten: oneOf wird zu einem Select mit MenuItems, string wird zu einem TextField, format: "date" wird zu einem Datepicker. Das uiSchema steuert deaktivierte Zustaende und Hilfstexte. Die extraErrors steuern Fehlermeldungen.

Die noValidate-Prop sagt RJSF, clientseitige JSON-Schema-Validierung komplett zu ueberspringen. Der Server ist der einzige Validierer. Du uebergibst trotzdem die validator-Prop (RJSF braucht sie fuer interne Typ-Aufloesung), aber sie blockiert nie das Absenden.

Warum RJSF?

Wir haben mehrere schema-getriebene Form-Renderer evaluiert, bevor wir uns fuer RJSF entschieden haben:

LibrarySchema-FormatServer-ErrorsMUI-Support
react-jsonschema-formJSON Schema + uiSchemaextraErrors-Prop (beste)Offizielles @rjsf/mui
JSON FormsJSON Schema + UI SchemaadditionalErrors-PropOffizielle Renderer
data-driven-formsEigenes flaches SchemaFinal Form InternalsOffizieller Mapper
uniformsJSON Schema via BridgeError-Object-MappingOffizielles Theme

RJSF hat aus drei Gruenden gewonnen: Die extraErrors-Prop ist speziell fuer serverseitige Validierung gebaut (du uebergibst feldbezogene __errors-Arrays und sie werden als MUI-FormHelperText gerendert), die noValidate-Prop deaktiviert sauber die clientseitige Validierung, und das Schema kann bei jedem Render ausgetauscht werden ohne Probleme. Wenn der Server nach einem Artenwechsel ein komplett anderes Schema zurueckgibt, baut RJSF den Form-Tree von Grund auf neu. Keine veralteten Felder, kein verwaister State.

Was ist mit OpenAPI und GraphQL?

Wenn du Code-Generierung nutzt (wie wir in Typesafe API Code Generation for React in 2026 besprochen haben), fragst du dich vielleicht, wie dieses Pattern reinpasst.

OpenAPI

OpenAPI 3.0 kann konditionale Validierungsregeln wie "wenn die Art CITES Anhang I ist, ministerielle Genehmigung erfordern" nicht ausdruecken. Das Schema ist statisch. OpenAPI 3.1 hat JSON Schema 2020-12 uebernommen, das if/then/else, dependentRequired und dependentSchemas unterstuetzt, aber die Tool-Unterstuetzung hinkt noch hinterher. Code-Generatoren wie Hey API und Orval generieren keinen konditionalen Validierungscode aus diesen Keywords.

Der pragmatische Ansatz: Nutze deine OpenAPI-Spec fuer Types und SDK-Generierung, aber behandle den /form-state-Endpoint als separaten Belang. Definiere ihn in deiner Spec wie jeden anderen Endpoint. Der Response-Typ umschliesst schema, uiSchema und extraErrors als opake JSON-Objekte. Deine generierte SDK-Funktion gibt sie zurueck, und du reichst sie direkt an RJSF weiter.

yaml
/transfer/form-state:
post:
summary: Form-Schema, UI-State und Validierung fuer einen Transfer abrufen
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/TransferInput"
responses:
"200":
content:
application/json:
schema:
type: object
required: [schema, uiSchema, extraErrors]
properties:
schema:
type: object
description: JSON Schema das die aktuellen Formularfelder beschreibt
uiSchema:
type: object
description: RJSF uiSchema fuer disabled/hidden/help-Zustaende
extraErrors:
type: object
description: RJSF ErrorSchema fuer Server-Validierungsfehler
GraphQL

GraphQL hat die gleiche Einschraenkung. Das Schema-Typsystem gibt dir Non-Null-Constraints und Enum-Werte, kann aber keine felduebergreifenden konditionalen Regeln ausdruecken.

Das Pattern uebersetzt sich in eine Query, die die drei RJSF-Objekte als JSON-Scalars zurueckgibt:

graphql
scalar JSON
input TransferInput {
speciesId: ID
destinationId: ID
transportMethod: String
transportDate: Date
healthCertificateDate: Date
ministerialApprovalRef: String
}
type TransferFormState {
schema: JSON!
uiSchema: JSON!
extraErrors: JSON!
}
type Query {
transferFormState(input: TransferInput!): TransferFormState!
}

Der Tradeoff ist, dass JSON-Scalars fuer das GraphQL-Typsystem opak sind. Du verlierst Type Safety auf der Response-Shape. In der Praxis ist das in Ordnung, weil RJSF der Consumer ist und die Schema-Struktur zur Render-Zeit validiert. Die Alternative (jeden Feldzustand als typisierten GraphQL-Typ zu modellieren) ist moeglich, schafft aber ein paralleles Typsystem, das mit den Erwartungen von RJSF synchron bleiben muss.

Tradeoffs

Dieses Pattern ist nicht kostenlos. Hier ist, was du eintauschst:

Netzwerk-Latenz bei jeder Feldaenderung. Jede relevante Interaktion feuert einen Request. Bei langsamen Verbindungen oder VPNs mit hoher Latenz ist das spuerbar. Gegenmassnahmen: aggressives Debouncing, clientseitige Vorvalidierung fuer offensichtliche Dinge (leere Pflichtfelder), SWR-Caching, Edge-Deployment.

Kein Offline-Support. Server-Driven Forms funktionieren grundsaetzlich nicht offline. Wenn das Netzwerk nicht verfuegbar ist, kann das Formular nicht validieren oder Optionen aktualisieren. Wenn du Offline-First-Formulare brauchst, ist dieses Pattern die falsche Wahl.

Komplexitaet im Vertrag. Das Backend generiert jetzt dynamisch JSON Schema. Das ist mehr Arbeit als eine flache Fehlerliste zurueckzugeben, aber der Gewinn ist, dass das Frontend null Custom-Rendering-Logik braucht.

Testen erfordert einen Server. Du kannst Formularverhalten nicht isoliert mit Unit-Tests allein testen. Du brauchst einen laufenden Server oder einen Mock, der realistische Form-State-Responses zurueckgibt. Integrationstests werden wichtiger.

Wann es sich lohnt
  • Formulare, die Daten aus mehreren Backend-Systemen ziehen (Artenregister, Einrichtungsdatenbanken, Regulierungs-APIs)
  • Komplexe felduebergreifende Abhaengigkeiten, die kaskadieren (eine Feldaenderung invalidiert oder veraendert andere)
  • Geschaeftsregeln, die sich haeufig aendern (neue Regulierungen, aktualisierte Constraints)
  • Formulare mit server-only Validierung (Eindeutigkeitspruefungen, Verfuegbarkeitspruefungen, externe API-Calls)
  • Multi-Tenant-Plattformen, bei denen jeder Mandant unterschiedliche Formularkonfigurationen hat
Wann nicht
  • Einfache statische Formulare (Login, Kontakt, Registrierung)
  • Offline-First-Anwendungen
  • Formulare, bei denen alle Regeln in einem clientseitigen Schema ausdrueckbar sind (Zod, Yup)
  • Formulare ohne felduebergreifende Abhaengigkeiten

Das mentale Modell

FrageWer antwortet
Welche Zielorte haben Gehege fuer diese Art?Server (generiert oneOf im Schema)
Ist das Gesundheitszertifikat am Transportdatum noch gueltig?Server (gibt extraErrors zurueck)
Braucht diese Art ministerielle Genehmigung?Server (fuegt Feld zum Schema + required hinzu)
Soll das Zielort-Dropdown deaktiviert sein?Server (setzt ui:disabled im uiSchema)
Wie rendere ich all das oben?RJSF (liest schema, uiSchema, extraErrors)
Wie zeige ich einen Ladezustand waehrend der Re-Validierung?Frontend (TanStack Query isFetching)
Soll ich das Gewichts-Eingabefeld debouncen?Frontend (useDebouncedValue)

Das Frontend wird zum Durchreicher. Der Server generiert ein JSON Schema, ein uiSchema und ein Error-Objekt. RJSF rendert sie als MUI-Komponenten. Du schreibst die Validierungslogik genau einmal im Backend und bekommst konsistentes Verhalten, egal ob der Request vom React-Formular, einer mobilen App oder einem direkten API-Consumer kommt.

Die einzigen echten Kosten sind der zusaetzliche Netzwerk-Roundtrip bei Feldaenderungen. Fuer Formulare wie den Transferantrag, bei dem die Regeln fuenf verschiedene Datenquellen umspannen und sich mit jedem Regulierungs-Update aendern, ist dieser Preis nicht mal eine Frage.


S
Geschrieben von
Sascha Becker
Weitere Artikel