# Validation des formulaires HTML dans le navigateur et sur le serveur

> Validez un formulaire avec required, type, pattern et min, stylisez les erreurs avec :user-invalid, et imposez les mêmes règles sur le serveur avec des définitions de champs.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/html-form-submission/validation

Utilisez les contraintes intégrées au HTML, comme `required`, `type="email"`, `min`, `max` et `pattern`, pour que le navigateur bloque les saisies incorrectes avant tout envoi. Imposez ensuite les mêmes règles sur le serveur, car les bots et les scripts contournent entièrement le navigateur. Avec Ikiform, définissez les champs du formulaire et chaque envoi est vérifié : un envoi incorrect reçoit `422 validation_failed` avec un message par champ, que vous pouvez afficher avec `setCustomValidity`.

L'exemple est un formulaire de réservation d'atelier. Il contient le genre de champs où la validation se rend utile : une taille d'équipe avec des limites, une date qui ne peut pas être passée, un numéro de téléphone dans un seul format et un code de réservation de forme fixe.

## Ce qui change ici

Il y a deux validateurs, et ils ne voient pas les mêmes requêtes. Le navigateur ne vérifie que les personnes qui utilisent votre page, avec ou sans scripts. Le serveur vérifie tout, y compris les bots qui envoient directement à l'endpoint et votre propre code qui appelle `form.submit()`, lequel ignore les vérifications du navigateur. Le travail consiste à faire dire la même chose aux deux.

Schéma : Un envoi qui passe par la validation du navigateur puis par la validation des champs d'Ikiform

1. Une personne envoie le formulaire, et le navigateur vérifie les contraintes du HTML. Un champ invalide reçoit un message et rien n'est envoyé.
2. Un envoi valide atteint Ikiform, qui le vérifie par rapport à la définition de champs du formulaire et stocke des valeurs typées.
3. Un bot ou un script envoie directement à l'endpoint, donc aucune vérification du navigateur ne s'exécute.
4. Ikiform le vérifie par rapport à la même définition et le refuse avec 422 avant que quoi que ce soit ne soit stocké.

Le niveau navigateur est fait pour les personnes. C'est le niveau serveur qui tient.

## Le niveau navigateur

Formulaire : Book a workshop

- **Votre nom** (`name`, text, obligatoire)
- **E-mail professionnel** (`email`, email, obligatoire)
- **Nombre de participants** (`team_size`, number, obligatoire)
- **Date souhaitée** (`date`, date, obligatoire)
- **Téléphone** (`phone`, phone)
- **Code de réservation** (`code`, text)

La définition qu'impose Ikiform. Essayez une taille d'équipe de 25, une date antérieure au 2 novembre, ou un code comme `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>
```

Chaque attribut correspond à un état de validité, que vous pouvez lire depuis `input.validity` dans la console :

| Attribut | État de validité | Remarques |
| --- | --- | --- |
| `required` | `valueMissing` | Pour un groupe de boutons radio, un seul radio avec `required` couvre le groupe |
| `type="email"`, `type="url"` | `typeMismatch` | Permissif : `a@b` passe pour un e-mail dans les navigateurs |
| `pattern` | `patternMismatch` | Doit correspondre à toute la valeur. Ignoré sur un champ facultatif vide |
| `minlength`, `maxlength` | `tooShort`, `tooLong` | Vérifié seulement après que la personne a modifié la valeur |
| `min`, `max` | `rangeUnderflow`, `rangeOverflow` | Nombres, dates et heures |
| `step` | `stepMismatch` | `step="1"` refuse les décimales |
| Saisie non analysable | `badInput` | Des lettres tapées dans un champ numérique dans certains navigateurs |
| `setCustomValidity()` | `customError` | Tout message que vous définissez, jusqu'à ce que vous le remettiez à vide |

### Des patterns dans deux moteurs

Les navigateurs compilent `pattern` avec le flag `v`, plus strict que les expressions régulières que la plupart des gens écrivent : un `-` littéral dans une classe de caractères doit être échappé, comme dans `[a-z\-]`. Ikiform compile le `pattern` d'un champ avec le flag `u` et le fait aussi correspondre à toute la valeur. Écrivez des patterns valides dans les deux, comme le code de réservation ci-dessus, et testez chacun dans l'aperçu du formulaire avant de le publier.

### Styliser les erreurs

`:invalid` correspond à un champ obligatoire vide dès le chargement de la page, donc tous les champs s'affichent en rouge avant que quiconque ne tape. `:user-invalid` ne correspond qu'après que la personne a interagi avec le champ ou tenté d'envoyer, ce qui est presque toujours ce que vous voulez :

`workshop.html`:

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

## Le niveau serveur

Collez la définition ci-dessus dans l'onglet **Fields** du formulaire. Dès lors, Ikiform valide chaque envoi par rapport à elle, quelle qu'en soit la source, et stocke des valeurs converties : `team_size` comme nombre, `phone` sans séparateurs, `date` au format `YYYY-MM-DD`. Un envoi qui enfreint une règle est refusé, rien n'est stocké, et la réponse indique quel champ a échoué :

| Champ | Envoyé | Message |
| --- | --- | --- |
| `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` | N'importe quelle valeur | `Unknown field "website"` |

La dernière ligne compte. Avec des champs définis, Ikiform refuse tout champ absent de la définition, donc un input renommé échoue de façon visible au lieu d'être stocké sous le mauvais nom. Les champs commençant par `_` et le honeypot ne comptent pas. Chaque type et chaque règle figurent dans [Champs et validation](https://ikiform.com/fr/docs/fields.md).

### Avec un envoi classique

Aucun code nécessaire. Un envoi classique refusé affiche la page d'erreur hébergée d'Ikiform avec le statut `422`, chaque problème sous le libellé de votre champ, et un lien vers la page d'où vient l'envoi. Le bouton **Back** du navigateur conserve généralement ce que la personne a saisi ; le lien recharge le formulaire vide.

### Afficher les erreurs du serveur sur les champs

Avec un script, injectez chaque message du serveur dans l'interface d'erreur propre au navigateur. `setCustomValidity` marque le champ comme invalide avec ce texte, et `reportValidity` l'affiche sous forme de bulle habituelle et place le focus sur le premier :

`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);
});
```

- Effacez le message personnalisé sur `input`. Un champ avec une erreur personnalisée reste invalide jusqu'à ce que vous la remettiez à une chaîne vide, et la personne ne peut alors plus du tout envoyer.
- Les vérifications du navigateur s'exécutent toujours avant l'événement `submit`, donc le serveur ne voit que les envois qui les ont passées.
- Si l'erreur n'a pas de `fields`, comme `429 rate_limited`, `reportValidity()` renvoie `true` et le script se rabat sur le message général.

## Vérifier les règles du serveur avant l'envoi

Pour repérer avant la requête les règles propres au serveur, comme une liste de domaines d'e-mail, chargez la définition publique du formulaire et exécutez le `validate` du [SDK](https://ikiform.com/fr/docs/sdk.md), qui applique dans le navigateur les mêmes règles et messages de champ :

`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();
});
```

> Considérez cela comme un confort. Le serveur reste la source de vérité, et `getSchema` n'expose que les définitions de champs, qui sont de toute façon publiques : n'importe qui peut les lire depuis `/f/<formId>/schema`.

## Vérifiez que tout fonctionne

1. Dans **Allowed domains** du formulaire, ajoutez le domaine de votre site, plus `localhost` pendant le développement. Laissez vide pour accepter les envois de partout.
2. Envoyez le formulaire vide. Le navigateur l'arrête à **Votre nom** et rien n'apparaît dans le panneau Réseau.
3. Retirez `max` de la taille d'équipe dans les outils de développement, saisissez `25` et envoyez. L'envoi classique affiche la page d'erreur d'Ikiform ; l'envoi par script affiche **Must be at most 20** sur le champ.
4. Envoyez avec curl, sans passer par le navigateur : `curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx`, avec votre propre domaine autorisé comme origine. La réponse est un `422` avec un message pour `team_size` et pour chaque champ obligatoire manquant.
5. Ouvrez **Submissions** dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.

## Pièges

| Problème | Cause et solution |
| --- | --- |
| Le formulaire ne s'envoie plus après une erreur du serveur | Un message de validité personnalisé n'a jamais été effacé. Réinitialisez-le avec `setCustomValidity("")` sur `input` |
| `minlength` ne se déclenche pas sur les valeurs préremplies | `tooShort` ne s'applique qu'après que la personne a modifié le champ. La règle du serveur le rattrape |
| Un `pattern` est entièrement ignoré | Il est invalide avec le flag `v`, et les navigateurs ignorent entièrement un pattern invalide. Échappez `-` et les autres caractères spéciaux dans les classes |
| Les erreurs s'affichent avant que quiconque ne tape | Le CSS utilise `:invalid`. Passez à `:user-invalid` |
| `422 validation_failed` | L'envoi ne correspond pas à la définition des champs du formulaire. `error.fields` nomme chaque champ et la raison. Faites correspondre le `name` de l'input à l'id du champ. |

Le déroulement complet d'un envoi se trouve dans la [vue d'ensemble](https://ikiform.com/fr/guides/html-form-submission.md#validation), et [Erreurs](https://ikiform.com/fr/docs/errors.md) liste chaque code qu'un envoi peut recevoir.

## Questions

### La validation des formulaires HTML suffit-elle à elle seule ?

Non. Elle ne s'exécute que dans le navigateur sur votre page, et les bots ou les scripts envoient directement à l'endpoint. Validez aussi sur le serveur. Avec Ikiform, définissez les champs du formulaire et chaque envoi est vérifié.

### Comment afficher un message d'erreur du serveur sur un input ?

Appelez input.setCustomValidity(message) puis form.reportValidity(), qui affiche le message dans la bulle habituelle du navigateur. Effacez-le avec setCustomValidity("") quand la personne modifie le champ.

### Quelle est la différence entre :invalid et :user-invalid ?

:invalid correspond à tout champ qui ne respecte pas ses contraintes, y compris les champs obligatoires vides au chargement de la page. :user-invalid ne correspond qu'après que la personne a interagi avec le champ ou tenté d'envoyer.

### Que renvoie Ikiform quand un champ échoue à la validation ?

422 validation_failed, sans rien stocker. Les requêtes JSON reçoivent error.fields, qui associe chaque identifiant de champ à ses messages ; les envois de formulaire classiques reçoivent une page d'erreur hébergée qui les liste par libellé de champ.

## Autres guides

- [Envoi de formulaire HTML](https://ikiform.com/fr/guides/html-form-submission.md): Ce que fait un navigateur quand un formulaire est envoyé : quels champs il transmet, comment il les encode, ce que le serveur doit répondre, et comment Ikiform répond.
- [Sans rechargement de page](https://ikiform.com/fr/guides/html-form-submission/without-page-reload.md): Envoyez un formulaire avec fetch et FormData pour que le visiteur reste sur la page, avec erreurs de champ, état de chargement, nouvelles tentatives et solution de repli quand les scripts échouent.
- [L'attribut action](https://ikiform.com/fr/guides/html-form-submission/action-attribute.md): Ce que fait l'attribut action d'un formulaire, comment les valeurs relatives et vides sont résolues, comment formaction envoie un formulaire à deux endroits, et pourquoi les hébergeurs statiques répondent 405.
- [Redirection après envoi](https://ikiform.com/fr/guides/html-form-submission/redirect-after-submit.md): Envoyez les visiteurs vers votre propre page de remerciement après l'envoi d'un formulaire : la redirection 303, le champ _redirect, les domaines autorisés et l'effet d'un rafraîchissement.
