Back to flin
flin

Validateurs de corps de requête

Comment les blocs validate de FLIN appliquent la sécurité de type, les contraintes et les règles métier sur les données de requête entrantes -- validation déclarative qui s'exécute avant votre code de gestionnaire et retourne des réponses d'erreur structurées.

Thales & Claude | March 30, 2026 9 min flin
EN/ FR/ ES
flinvalidatorsrequestvalidation

La validation des entrées est la première ligne de défense de toute application web. Des données invalides causent des plantages, corrompent les bases de données, permettent des attaques par injection et créent des bugs subtils qui se manifestent en production des semaines après le déploiement. Pourtant, dans la plupart des frameworks, la validation est un code après coup dispersé dans les gestionnaires, dupliqué entre les points de terminaison et appliqué de manière incohérente.

Les blocs validate de FLIN sont déclaratifs, composables et appliqués avant que votre code de gestionnaire ne s'exécute. Vous déclarez à quoi le corps de la requête doit ressembler, et FLIN rejette automatiquement les requêtes malformées avec des réponses d'erreur structurées. Pas de bibliothèque de validation. Pas de vérification manuelle. Pas moyen d'oublier.

Le bloc validate

Un bloc validate déclare la forme et les contraintes attendues du corps de la requête :

flinroute POST {
    validate {
        name: text @required @minLength(2) @maxLength(100)
        email: text @required @email
        age: int @min(13) @max(120)
        role: text @one_of("user", "admin", "moderator")
        bio: text @maxLength(500)
    }

    // Si nous arrivons ici, toutes les validations sont passées
    // body.name est garanti être une chaîne de 2-100 caractères
    // body.email est garanti être un e-mail valide
    // body.age est garanti être un entier entre 13 et 120

    user = User {
        name: body.name,
        email: body.email,
        age: body.age,
        role: body.role || "user",
        bio: body.bio || ""
    }
    save user

    response { status: 201, body: user }
}

Si un champ échoue à la validation, FLIN retourne un 400 Bad Request avec des informations d'erreur détaillées :

json{
    "error": "Validation failed",
    "status": 400,
    "fields": {
        "name": "Must be at least 2 characters",
        "email": "Must be a valid email address",
        "age": "Must be at least 13"
    }
}

Le code du gestionnaire ne s'exécute jamais. Les données invalides n'atteignent jamais la base de données. La réponse d'erreur indique au client exactement quels champs ont échoué et pourquoi.

Décorateurs disponibles

FLIN fournit un ensemble complet de décorateurs de validation :

Décorateurs de type

TypeDescriptionCoercition
textValeur chaîneDepuis tout type via to_text()
intValeur entièreDepuis chaîne via to_int()
floatValeur à virgule flottanteDepuis chaîne via to_float()
boolValeur booléenneDepuis "true"/"false"/"1"/"0"
fileFichier téléverséDepuis partie multipart
[type]Tableau de valeursAnalysé comme tableau JSON

Décorateurs de contrainte

flinvalidate {
    // Champs obligatoires
    name: text @required                  // Doit être présent et non vide

    // Contraintes de chaîne
    slug: text @minLength(3) @maxLength(50) @pattern("^[a-z0-9-]+$")
    email: text @email                    // Doit correspondre au format e-mail
    url: text @url                        // Doit être une URL valide
    phone: text @phone                    // Doit être un numéro valide

    // Contraintes numériques
    age: int @min(0) @max(150)
    price: float @min(0.01) @max(999999.99)
    quantity: int @required @min(1) @max(10000)

    // Contraintes d'énumération
    status: text @one_of("active", "inactive", "pending")
    priority: int @one_of(1, 2, 3, 4, 5)

    // Contraintes de fichier
    avatar: file @max_size("5MB") @allow_types("image/png", "image/jpeg")
    documents: [file] @max_count(10) @max_size("25MB")

    // Message de validation personnalisé
    password: text @required @minLength(8) @message("Password must be at least 8 characters")
}

Le décorateur @required

Les champs sans @required sont optionnels. S'ils sont absents de la requête, ils prennent une valeur vide ("" pour text, 0 pour int, false pour bool, none pour file).

flinvalidate {
    name: text @required          // Doit être présent
    nickname: text                // Optionnel, valeur par défaut ""
    email: text @required @email  // Doit être présent ET valide
}

Le décorateur @pattern

Pour une validation qui ne correspond pas à un décorateur intégré, utilisez @pattern avec une expression régulière :

flinvalidate {
    slug: text @required @pattern("^[a-z0-9][a-z0-9-]*[a-z0-9]$")
    postal_code: text @pattern("^[0-9]{5}$")
    hex_color: text @pattern("^#[0-9a-fA-F]{6}$")
}

Coercition de type

Le bloc validate effectue la coercition de type pour les données encodées en formulaire. Les valeurs de formulaire sont toujours des chaînes, mais le validateur les convertit au type déclaré :

flinvalidate {
    quantity: int @required @min(1)
    // Le formulaire envoie "5" (chaîne) -> le validateur convertit en 5 (int)

    price: float @required @min(0.01)
    // Le formulaire envoie "29.99" (chaîne) -> le validateur convertit en 29.99 (float)

    active: bool
    // Le formulaire envoie "true" (chaîne) -> le validateur convertit en true (bool)
}

Les requêtes JSON ont déjà des valeurs typées, donc la coercition est un no-op. Cela signifie que le même bloc validate fonctionne pour les requêtes JSON et les requêtes encodées en formulaire.

Validation d'objets imbriqués

Pour les corps de requête complexes avec des objets imbriqués :

flinvalidate {
    user: {
        name: text @required @minLength(2)
        email: text @required @email
    }
    address: {
        street: text @required
        city: text @required
        postal_code: text @required @pattern("^[0-9]{5}$")
        country: text @required @one_of("CI", "SN", "NG", "GH", "KE")
    }
    items: [{
        product_id: int @required
        quantity: int @required @min(1)
    }]
}

Le client envoie :

json{
    "user": { "name": "Thales", "email": "[email protected]" },
    "address": { "street": "Rue des Jardins", "city": "Abidjan", "postal_code": "01234", "country": "CI" },
    "items": [
        { "product_id": 1, "quantity": 2 },
        { "product_id": 5, "quantity": 1 }
    ]
}

Chaque champ imbriqué est validé individuellement. Les erreurs sont rapportées avec une notation par chemin pointé :

json{
    "error": "Validation failed",
    "fields": {
        "address.postal_code": "Must match pattern ^[0-9]{5}$",
        "items[1].quantity": "Must be at least 1"
    }
}

Comment la validation est implémentée

Le bloc validate se compile en une fonction de validation qui s'exécute avant le gestionnaire :

rustpub struct ValidateField {
    name: String,
    field_type: FieldType,
    required: bool,
    constraints: Vec<Constraint>,
    custom_message: Option<String>,
}

pub enum Constraint {
    MinLength(usize),
    MaxLength(usize),
    Min(f64),
    Max(f64),
    Pattern(Regex),
    Email,
    Url,
    Phone,
    OneOf(Vec<Value>),
    MaxSize(usize),
    AllowTypes(Vec<String>),
    MaxCount(usize),
}

fn validate_body(
    body: &Value,
    fields: &[ValidateField],
) -> Result<Value, ValidationErrors> {
    let mut errors = HashMap::new();
    let mut coerced = body.clone();

    for field in fields {
        let value = body.get(&field.name);

        // Vérifier obligatoire
        if field.required && (value.is_none() || value == Some(&Value::Empty)) {
            errors.insert(field.name.clone(), "This field is required".into());
            continue;
        }

        if let Some(val) = value {
            // Coercition de type
            let typed = coerce(val, &field.field_type)?;

            // Vérification des contraintes
            for constraint in &field.constraints {
                if let Err(msg) = check_constraint(&typed, constraint) {
                    errors.insert(
                        field.name.clone(),
                        field.custom_message.as_deref().unwrap_or(&msg).to_string(),
                    );
                    break;
                }
            }

            coerced.set(&field.name, typed);
        }
    }

    if errors.is_empty() {
        Ok(coerced)
    } else {
        Err(ValidationErrors { fields: errors })
    }
}

La fonction de validation : 1. Itère sur tous les champs déclarés. 2. Vérifie @required en premier. 3. Effectue la coercition de type. 4. Évalue chaque contrainte dans l'ordre (s'arrête au premier échec par champ). 5. Retourne soit le corps coercé, soit une carte d'erreurs par champ.

Schémas de validation réutilisables

Lorsque plusieurs points de terminaison partagent les mêmes règles de validation, FLIN permet d'extraire les validateurs en schémas réutilisables :

flin// Définir une fois
schema UserInput {
    name: text @required @minLength(2) @maxLength(100)
    email: text @required @email
    role: text @one_of("user", "admin", "moderator")
}

// Utiliser dans plusieurs routes
route POST {
    validate UserInput
    // ...
}

route PUT {
    validate UserInput
    // Même validation, gestionnaire différent
}

Validation vs gardes

La validation et les gardes servent des objectifs différents et se complètent :

Les gardes protègent l'accès : qui peut appeler ce point de terminaison ? Les gardes s'exécutent avant même que le corps de la requête ne soit analysé. Un utilisateur non authentifié est rejeté avant que son corps de requête ne soit lu.

Les validateurs protègent les données : quelle forme doivent avoir les entrées ? Les validateurs s'exécutent après que les gardes soient passés et que le corps soit analysé. Ils s'assurent que les données sont bien formées avant que le gestionnaire ne les traite.

flinguard auth                    // Qui : utilisateurs authentifiés uniquement
guard role("admin")           // Qui : administrateurs uniquement
guard rate_limit(10, 60)      // Fréquence : 10/minute

route POST {
    validate {                // Quoi : données utilisateur bien formées
        name: text @required
        email: text @required @email
    }

    // Si nous arrivons ici :
    // 1. L'utilisateur est authentifié (guard auth)
    // 2. L'utilisateur est admin (guard role)
    // 3. La requête est dans la limite de débit (guard rate_limit)
    // 4. Le corps a un nom et un e-mail valides (validate)
}

Quatre couches de protection, chacune exprimée de manière déclarative, chacune appliquée automatiquement.

Cohérence des réponses d'erreur

Chaque réponse d'erreur de validation suit la même structure :

json{
    "error": "Validation failed",
    "status": 400,
    "fields": {
        "field_name": "Human-readable error message"
    }
}

Cette cohérence signifie que le code frontend peut gérer les erreurs de validation de manière générique :

javascript// Code frontend (tout framework)
const response = await fetch('/api/users', { method: 'POST', body: data });
if (response.status === 400) {
    const { fields } = await response.json();
    Object.entries(fields).forEach(([field, message]) => {
        showError(field, message);
    });
}

Les noms de champs dans la réponse d'erreur correspondent aux noms de champs dans le corps de la requête. Pas de mapping nécessaire.

La validation de FLIN n'est pas une bibliothèque que vous installez. Ce n'est pas un middleware que vous configurez. C'est une fonctionnalité du langage qui se compile en code de vérification de type efficace, produit des messages d'erreur clairs et ne peut pas être contournée. Chaque point de terminaison API qui utilise un bloc validate est protégé contre les entrées malformées, à chaque fois, automatiquement.

Dans le prochain article, nous regardons comment nous avons vérifié que toutes ces fonctionnalités de sécurité fonctionnent correctement : 75 tests de sécurité couvrant l'authentification, l'autorisation, la limitation de débit, la validation des entrées et les opérations cryptographiques.


Ceci est la partie 113 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.

Navigation de la série : - [112] Authentification WhatsApp OTP pour l'Afrique - [113] Validateurs de corps de requête (vous êtes ici) - [114] 75 tests de sécurité : comment nous avons tout vérifié - [115] Gardes personnalisés et middleware de sécurité

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles