# HTML-Formularvalidierung im Browser und auf dem Server

> Validiere ein Formular mit required, type, pattern und min, gestalte Fehler mit :user-invalid und setze dieselben Regeln mit Felddefinitionen auf dem Server durch.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/html-form-submission/validation

Nutze die eingebauten Constraints von HTML wie `required`, `type="email"`, `min`, `max` und `pattern`, damit der Browser falsche Eingaben blockiert, bevor etwas gesendet wird. Setze dann dieselben Regeln auf dem Server durch, denn Bots und Skripte umgehen den Browser komplett. Definiere bei Ikiform die Felder des Formulars, und jeder Post wird geprüft: Ein fehlerhafter bekommt `422 validation_failed` mit einer Meldung pro Feld, die du mit `setCustomValidity` anzeigen kannst.

Das Beispiel ist ein Buchungsformular für einen Workshop. Es hat die Art Felder, bei denen sich Validierung lohnt: eine Teamgröße mit Grenzen, ein Datum, das nicht in der Vergangenheit liegen darf, eine Telefonnummer in einem Format und ein Buchungscode mit fester Form.

## Was hier anders ist

Es gibt zwei Validierer, und sie sehen nicht dieselben Requests. Der Browser prüft nur Menschen, die deine Seite nutzen, mit oder ohne Skripte. Der Server prüft alles, auch Bots, die direkt an den Endpunkt senden, und deinen eigenen Code, der `form.submit()` aufruft und damit die Prüfungen des Browsers überspringt. Die Arbeit besteht darin, beide dasselbe sagen zu lassen.

Diagramm: Ein Post, der die Browservalidierung und dann die Feldvalidierung von Ikiform durchläuft

1. Eine Person sendet das Formular ab, und der Browser prüft die Constraints im HTML. Ein ungültiges Feld bekommt eine Meldung, und nichts wird gesendet.
2. Ein gültiger Post erreicht Ikiform, das ihn gegen die Felddefinition des Formulars prüft und typisierte Werte speichert.
3. Ein Bot oder Skript sendet direkt an den Endpunkt, also läuft keine Browserprüfung.
4. Ikiform prüft ihn gegen dieselbe Definition und lehnt ihn mit 422 ab, bevor etwas gespeichert wird.

Die Browserschicht ist für Menschen. Die Serverschicht ist die, die hält.

## Die Browserschicht

Formular: Book a workshop

- **Dein Name** (`name`, text, Pflichtfeld)
- **Geschäftliche E-Mail** (`email`, email, Pflichtfeld)
- **Teilnehmende** (`team_size`, number, Pflichtfeld)
- **Wunschtermin** (`date`, date, Pflichtfeld)
- **Telefon** (`phone`, phone)
- **Buchungscode** (`code`, text)

Die Definition, die Ikiform durchsetzt. Probier eine Teamgröße von 25, ein Datum vor dem 2. November oder einen Code wie `ws-12`.

`workshop.html`:

```html
<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Your name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="team_size">People attending</label>
  <input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />

  <label for="date">Preferred date</label>
  <input id="date" name="date" type="date" min="2026-11-02" required />

  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
    pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />

  <label for="code">Booking code</label>
  <input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request the workshop</button>
</form>
```

Jedes Attribut entspricht einem Gültigkeitsstatus, den du in der Konsole aus `input.validity` lesen kannst:

| Attribut | Gültigkeitsstatus | Hinweise |
| --- | --- | --- |
| `required` | `valueMissing` | Bei einer Radio-Gruppe deckt ein Radio mit `required` die ganze Gruppe ab |
| `type="email"`, `type="url"` | `typeMismatch` | Locker: `a@b` gilt in Browsern als E-Mail |
| `pattern` | `patternMismatch` | Muss auf den ganzen Wert passen. Wird bei einem leeren optionalen Feld ignoriert |
| `minlength`, `maxlength` | `tooShort`, `tooLong` | Wird erst geprüft, nachdem die Person den Wert bearbeitet hat |
| `min`, `max` | `rangeUnderflow`, `rangeOverflow` | Zahlen, Datumsangaben und Uhrzeiten |
| `step` | `stepMismatch` | `step="1"` lehnt Dezimalzahlen ab |
| Nicht parsebare Eingabe | `badInput` | In manchen Browsern Buchstaben in einem Zahlenfeld |
| `setCustomValidity()` | `customError` | Jede Meldung, die du setzt, bis du sie wieder auf leer setzt |

### Patterns in zwei Engines

Browser kompilieren `pattern` mit dem Flag `v`, das strenger ist als die regulären Ausdrücke, die die meisten schreiben: Ein literales `-` in einer Zeichenklasse muss maskiert werden, wie in `[a-z\-]`. Ikiform kompiliert das `pattern` eines Feldes mit dem Flag `u` und prüft ebenfalls den ganzen Wert. Schreib Patterns, die in beiden gültig sind, wie der Buchungscode oben, und teste jedes in der Formularvorschau, bevor du es veröffentlichst.

### Fehler gestalten

`:invalid` trifft ein leeres Pflichtfeld schon beim Laden der Seite, also zeigt jedes Feld Rot, bevor jemand tippt. `:user-invalid` trifft erst, nachdem die Person mit dem Feld interagiert oder das Absenden versucht hat, und das ist fast immer, was du willst:

`workshop.html`:

```html
<style>
  #workshop :user-invalid {
    border-color: #b42318;
  }
</style>
```

## Die Serverschicht

Füge die Definition oben im Tab **Fields** des Formulars ein. Ab dann validiert Ikiform jeden Post dagegen, aus jeder Quelle, und speichert umgewandelte Werte: `team_size` als Zahl, `phone` ohne Trennzeichen, `date` als `YYYY-MM-DD`. Ein Post, der eine Regel verletzt, wird abgelehnt, nichts wird gespeichert, und die Antwort sagt, welches Feld gescheitert ist:

| Feld | Gesendet | Nachricht |
| --- | --- | --- |
| `team_size` | `25` | `Must be at most 20` |
| `team_size` | `2.5` | `Enter a whole number` |
| `date` | `2026-10-30` | `Must be on or after 2026-11-02` |
| `phone` | `415 555 0123` | `Use international format, e.g. +14155550123` |
| `code` | `ws-12` | `Codes look like WS-1234` |
| `website` | Beliebiger Wert | `Unknown field "website"` |

Die letzte Zeile ist wichtig. Mit definierten Feldern lehnt Ikiform jedes Feld ab, das nicht in der Definition steht, sodass ein umbenannter Input laut scheitert, statt unter dem falschen Namen gespeichert zu werden. Felder, die mit `_` beginnen, und der Honeypot zählen nicht. Jeder Typ und jede Regel steht in [Felder und Validierung](https://ikiform.com/de/docs/fields.md).

### Mit einem einfachen Post

Kein Code nötig. Ein abgelehnter einfacher Post zeigt Ikiforms gehostete Fehlerseite mit dem Status `422`, jedes Problem unter dem Label deines Feldes und einen Link zur Seite, von der der Post kam. Der **Zurück**-Button des Browsers behält meist, was die Person eingetippt hat; der Link lädt das Formular leer neu.

### Serverfehler an den Feldern anzeigen

Mit einem Skript speist du jede Servermeldung in die eigene Fehleranzeige des Browsers ein. `setCustomValidity` markiert das Feld mit diesem Text als ungültig, und `reportValidity` zeigt sie als die übliche Blase und fokussiert die erste:

`workshop.js`:

```js
const form = document.querySelector("#workshop");

for (const control of form.elements) {
  control.addEventListener("input", () => control.setCustomValidity?.(""));
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(form),
  });
  const result = await response.json();
  if (result.ok) {
    form.replaceWith("Thanks. We'll confirm the date by email.");
    return;
  }
  const fields = result.error.fields ?? {};
  for (const [name, messages] of Object.entries(fields)) {
    form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
  }
  if (!form.reportValidity()) return;
  alert(result.error.message);
});
```

- Lösche die eigene Meldung bei `input`. Ein Feld mit einem eigenen Fehler bleibt ungültig, bis du ihn wieder auf einen leeren String setzt, und bis dahin kann die Person gar nicht absenden.
- Die Prüfungen des Browsers laufen weiterhin vor dem `submit`-Event, also sieht der Server nur Posts, die sie bestanden haben.
- Hat der Fehler keine `fields`, etwa bei `429 rate_limited`, gibt `reportValidity()` `true` zurück, und das Skript fällt auf die allgemeine Meldung zurück.

## Serverregeln vor dem Senden prüfen

Um reine Serverregeln wie eine Liste von E-Mail-Domains schon vor dem Request zu erwischen, lade die öffentliche Definition des Formulars und führe `validate` aus dem [SDK](https://ikiform.com/de/docs/sdk.md) aus, das dieselben Feldregeln und Meldungen im Browser anwendet:

`workshop.js`:

```js
import { getSchema, validate } from "@ikiform/sdk";

const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];

form.addEventListener("submit", (event) => {
  const errors = validate(fields, new FormData(form));
  for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
  if (!form.reportValidity()) event.preventDefault();
});
```

> Betrachte das als Komfort. Der Server bleibt die maßgebliche Instanz, und `getSchema` legt nur Felddefinitionen offen, die ohnehin öffentlich sind: Jeder kann sie unter `/f/<formId>/schema` lesen.

## Prüfen, ob es funktioniert

1. Füge unter **Allowed domains** des Formulars die Domain deiner Site hinzu, plus `localhost`, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
2. Sende das leere Formular ab. Der Browser stoppt es bei **Your name**, und im Network-Panel erscheint nichts.
3. Entferne `max` von der Teamgröße in den Entwicklertools, gib `25` ein und sende ab. Der einfache Post zeigt Ikiforms Fehlerseite; der per Skript zeigt **Must be at most 20** am Feld.
4. Sende mit curl und umgeh den Browser: `curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx`, mit deiner eigenen erlaubten Domain als Origin. Die Antwort ist `422` mit einer Meldung für `team_size` und jedes fehlende Pflichtfeld.
5. Öffne **Submissions** im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.

## Stolperfallen

| Problem | Ursache und Lösung |
| --- | --- |
| Das Formular sendet nach einem Serverfehler nie wieder ab | Eine eigene Gültigkeitsmeldung wurde nie gelöscht. Setze sie bei `input` mit `setCustomValidity("")` zurück |
| `minlength` greift bei vorausgefüllten Werten nicht | `tooShort` gilt erst, nachdem die Person das Feld bearbeitet hat. Die Serverregel fängt es ab |
| Ein `pattern` wird komplett ignoriert | Es ist mit dem Flag `v` ungültig, und Browser überspringen ein ungültiges Pattern komplett. Maskiere `-` und andere Sonderzeichen in Klassen |
| Fehler erscheinen, bevor jemand tippt | Das CSS nutzt `:invalid`. Wechsle zu `:user-invalid` |
| `422 validation_failed` | Der Post passt nicht zur Felddefinition des Formulars. `error.fields` nennt jedes Feld und den Grund. Gleiche den `name` des Eingabefelds an die Feld-ID an. |

Den vollständigen Ablauf findest du in der [Übersicht](https://ikiform.com/de/guides/html-form-submission.md#validation), und [Fehler](https://ikiform.com/de/docs/errors.md) listet jeden Code, den ein Post zurückbekommen kann.

## Fragen

### Reicht HTML-Formularvalidierung allein?

Nein. Sie läuft nur im Browser auf deiner Seite, und Bots oder Skripte senden direkt an den Endpunkt. Validiere auch auf dem Server. Definiere bei Ikiform die Felder des Formulars, und jeder Post wird geprüft.

### Wie zeige ich eine Fehlermeldung des Servers an einem Input an?

Ruf input.setCustomValidity(message) und dann form.reportValidity() auf, das die Meldung in der üblichen Blase des Browsers zeigt. Lösche sie mit setCustomValidity(""), wenn die Person das Feld bearbeitet.

### Was ist der Unterschied zwischen :invalid und :user-invalid?

:invalid trifft jedes Feld, das seine Constraints nicht erfüllt, auch leere Pflichtfelder beim Laden der Seite. :user-invalid trifft erst, nachdem die Person mit dem Feld interagiert oder das Absenden versucht hat.

### Was gibt Ikiform zurück, wenn ein Feld die Validierung nicht besteht?

422 validation_failed, und nichts wird gespeichert. JSON-Requests bekommen error.fields, das jede Feld-ID ihren Meldungen zuordnet; einfache Formular-Posts bekommen eine gehostete Fehlerseite, die sie nach Feldlabel auflistet.

## Weitere Anleitungen

- [Absenden von HTML-Formularen](https://ikiform.com/de/guides/html-form-submission.md): Was ein Browser beim Absenden eines Formulars tut: welche Felder er sendet, wie er sie kodiert, was der Server antworten sollte und wie Ikiform antwortet.
- [Ohne Neuladen der Seite](https://ikiform.com/de/guides/html-form-submission/without-page-reload.md): Sende ein Formular mit fetch und FormData, damit der Besucher auf der Seite bleibt, mit Feldfehlern, Ladezustand, Wiederholungen und einem Fallback, wenn Skripte versagen.
- [Das action-Attribut](https://ikiform.com/de/guides/html-form-submission/action-attribute.md): Was das action-Attribut eines Formulars tut, wie relative und leere Werte aufgelöst werden, wie formaction ein Formular an zwei Ziele schickt und warum statische Hosts mit 405 antworten.
- [Weiterleitung nach dem Absenden](https://ikiform.com/de/guides/html-form-submission/redirect-after-submit.md): Schick Besucher nach dem Absenden auf deine eigene Dankeseite: die 303-Weiterleitung, das Feld _redirect, welche Domains erlaubt sind und was Neuladen bewirkt.
