Catalogue Décider et valider

Valider un formulaire côté serveur

Vérifier que les données reçues d'un formulaire sont complètes et cohérentes avant de les enregistrer.

RecommandéN0 Révisée le

Récapitulatif des barreaux
Barreau Approche Coût Latence Données Déterministe Verdict
Règle et algorithme classique N0 — Règle et algorithme classique Schéma déclaratif, un message d'erreur par champ Nul <1 ms Rien ne sort Oui Recommandé
Modèle classique léger N1 — Modèle classique léger Barreau absent Il n'y a rien à apprendre. L'âge minimal, la longueur d'un pseudonyme, le format d'adresse accepté : ce sont des décisions écrites et opposables, pas des régularités à retrouver dans des données. Un classifieur entraîné sur les saisies passées apprendrait ce qui a été accepté hier, erreurs comprises, et ne saurait toujours pas dire quel champ reprendre.
Petit modèle spécialisé auto-hébergé N2 — Petit modèle spécialisé auto-hébergé Barreau absent Même raison qu'en N1, avec un service permanent à exploiter en plus. Un modèle auto-hébergé ne rend pas une règle plus juste : il rend son verdict plus cher à obtenir, plus lent à rendre, et impossible à relire dans le schéma que l'équipe maintient.
API de LLM généraliste N3 — API de LLM généraliste Barreau absent Une validation doit refuser de façon déterministe, et justifier son refus devant la personne dont elle refuse la saisie. Un modèle qui accepte parfois et refuse parfois la même saisie ne valide rien : il donne un avis. Deux envois identiques doivent recevoir la même réponse, et cette réponse doit se lire dans une règle qu'on peut produire — un schéma, une ligne de code, un test — et non dans une phrase dont on ne peut ni rejouer la production ni montrer le fondement. S'y ajoute que le contenu d'un formulaire est du texte écrit par la personne validée : le confier à un modèle qui lit ce texte comme une consigne, c'est la laisser peser sur le verdict qui la concerne.

N0 — Règle et algorithme classique Règle et algorithme classique Recommandé

Schéma déclaratif, un message d'erreur par champ

Coût
Nul
Latence
<1 ms

Preuve d’exécution : Code exécuté tel quel

Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.

Python

snippets/validate-a-form-server-side/n0.py
"""
Validate a form on the server: a declarative schema, one error per field.

Rung N0. Deterministic, standard library only. The whole point of this entry is
that a validator worth having fits in forty lines, so the rules stay readable
and every refusal can be explained to the person who typed the form.

Three decisions carry the design.

First, the schema is data, not code. It can be written next to the form, read by
someone who does not write Python, and compared with the JavaScript one that
guards the same form in the browser.

Second, the answer is a mapping of field name to message, never a boolean. A
form that answers "no" without saying which field is wrong sends the user
hunting, and sends the developer to the logs.

Third, one message per field: checks stop at the first broken rule. Telling
someone their password is too short *and* badly formed at once is noise; fix
the first thing, resubmit, see the next.
"""

import re

# The types a form field can hold once decoded. `bool` is excluded from
# `integer` on purpose: in Python a boolean *is* an int, and a checkbox is not
# an age.
TYPES = {
    "string": lambda v: isinstance(v, str),
    "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
}


def check(value, rule):
    """Return the first broken rule as a message, or None if the value passes."""
    kind = rule.get("type", "string")
    if not TYPES[kind](value):
        return f"must be of type {kind}"
    # For a string the bounds read as a length; for a number, as a value.
    size, unit = (len(value), " characters") if kind == "string" else (value, "")
    if "min" in rule and size < rule["min"]:
        return f"must be at least {rule['min']}{unit}"
    if "max" in rule and size > rule["max"]:
        return f"must be at most {rule['max']}{unit}"
    if "pattern" in rule and not re.fullmatch(rule["pattern"], value):
        return rule.get("message", "is not in the expected format")
    return None


def validate(data, schema):
    """
    Check a submitted form against a schema, and return {field: message}.

    An empty mapping means the form is valid. A missing key, an explicit None
    and an empty string are the same thing here, because that is what a browser
    posts for a field the user left alone.
    """
    errors = {}
    for field, rule in schema.items():
        value = data.get(field)
        if value is None or value == "":
            if rule.get("required"):
                errors[field] = "is required"
            continue
        message = check(value, rule)
        if message is not None:
            errors[field] = message
    return errors

JavaScript

snippets/validate-a-form-server-side/n0.js
/**
 * Validate a form on the server: a declarative schema, one error per field.
 *
 * Rung N0. Deterministic, no dependency. The whole point of this entry is that
 * a validator worth having fits in forty lines, so the rules stay readable and
 * every refusal can be explained to the person who typed the form.
 *
 * Three decisions carry the design.
 *
 * First, the schema is data, not code. It can be written next to the form, read
 * by someone who does not write JavaScript, and compared with the Python one
 * that guards the same form on the other side.
 *
 * Second, the answer is a mapping of field name to message, never a boolean. A
 * form that answers "no" without saying which field is wrong sends the user
 * hunting, and sends the developer to the logs.
 *
 * Third, one message per field: checks stop at the first broken rule. Telling
 * someone their password is too short *and* badly formed at once is noise; fix
 * the first thing, resubmit, see the next.
 */

// The types a form field can hold once decoded. `Number.isInteger` rejects
// NaN, Infinity and 1.5 in one go, which is what a form needs.
const TYPES = {
  string: (v) => typeof v === 'string',
  integer: (v) => Number.isInteger(v),
};

/** Return the first broken rule as a message, or null if the value passes. */
export function check(value, rule) {
  const kind = rule.type ?? 'string';
  if (!TYPES[kind](value)) return `must be of type ${kind}`;
  // For a string the bounds read as a length; for a number, as a value.
  const [size, unit] = kind === 'string' ? [value.length, ' characters'] : [value, ''];
  if (rule.min !== undefined && size < rule.min) return `must be at least ${rule.min}${unit}`;
  if (rule.max !== undefined && size > rule.max) return `must be at most ${rule.max}${unit}`;
  // Anchored, so a pattern matches the whole field and not a fragment of it.
  if (rule.pattern !== undefined && !new RegExp(`^(?:${rule.pattern})$`).test(value)) {
    return rule.message ?? 'is not in the expected format';
  }
  return null;
}

/**
 * Check a submitted form against a schema, and return {field: message}.
 *
 * An empty object means the form is valid. A missing key, an explicit null and
 * an empty string are the same thing here, because that is what a browser posts
 * for a field the user left alone.
 */
export function validate(data, schema) {
  const errors = {};
  for (const [field, rule] of Object.entries(schema)) {
    const value = data[field];
    if (value === undefined || value === null || value === '') {
      if (rule.required) errors[field] = 'is required';
      continue;
    }
    const message = check(value, rule);
    if (message !== null) errors[field] = message;
  }
  return errors;
}

Risques

Sortie de données
Rien ne sort
Déterminisme
Oui
Testabilité
Testable unitairement
Dépendance fournisseur
Aucune
Empreinte
Négligeable
Périmètre réglementaire
  • Aucun périmètre spécifique ajouté : la saisie ne quitte pas votre infrastructure
  • Les messages renvoyés nomment le champ et la règle enfreinte, jamais la valeur saisie : ce qui atterrit dans vos journaux d'erreur reste ce que vous y mettez

Point de rupture

Aucune règle ne dit si ce qui est saisi existe. Le test soumet `ada@no-such-mailbox.example` : la forme est correcte, le schéma accepte, et personne ne lit le courrier envoyé là. Le savoir demande une vérification externe — un message de confirmation, une interrogation du domaine — c'est-à-dire un autre besoin.

Quand monter d’un barreau

Des saisies bien formées mais fausses commencent à vous coûter quelque chose de visible : des comptes créés qui ne confirment jamais, des courriers qui reviennent. Aucun barreau au-dessus n'y répond ; ce qu'il vous faut est une vérification externe, qui est un autre besoin.

N1 — Modèle classique léger Modèle classique léger

Barreau absent

Il n'y a rien à apprendre. L'âge minimal, la longueur d'un pseudonyme, le format d'adresse accepté : ce sont des décisions écrites et opposables, pas des régularités à retrouver dans des données. Un classifieur entraîné sur les saisies passées apprendrait ce qui a été accepté hier, erreurs comprises, et ne saurait toujours pas dire quel champ reprendre.

N2 — Petit modèle spécialisé auto-hébergé Petit modèle spécialisé auto-hébergé

Barreau absent

Même raison qu'en N1, avec un service permanent à exploiter en plus. Un modèle auto-hébergé ne rend pas une règle plus juste : il rend son verdict plus cher à obtenir, plus lent à rendre, et impossible à relire dans le schéma que l'équipe maintient.

N3 — API de LLM généraliste API de LLM généraliste

Barreau absent

Une validation doit refuser de façon déterministe, et justifier son refus devant la personne dont elle refuse la saisie. Un modèle qui accepte parfois et refuse parfois la même saisie ne valide rien : il donne un avis. Deux envois identiques doivent recevoir la même réponse, et cette réponse doit se lire dans une règle qu'on peut produire — un schéma, une ligne de code, un test — et non dans une phrase dont on ne peut ni rejouer la production ni montrer le fondement. S'y ajoute que le contenu d'un formulaire est du texte écrit par la personne validée : le confier à un modèle qui lit ce texte comme une consigne, c'est la laisser peser sur le verdict qui la concerne.

Le verdict

RecommandéN0

N0 n'est pas ici le premier barreau d'un escalier, c'est le seul qui tienne. Une règle de formulaire est écrite avant d'être appliquée : elle se lit, elle se teste unitairement, et elle se montre à qui la conteste. Les trois barreaux au-dessus échangeraient cette propriété contre une décision qu'on ne peut ni rejouer ni justifier, ce qui est exactement ce qu'on demande à une validation.

Pour aller plus loin

Métadonnées