Un formulaire qui « marche chez moi » et casse à l’école, c’est presque toujours le même trou : le front envoie un champ, le back en attend un autre, et personne n’a écrit la règle.

Je ne commence pas par l’écran. Je commence par le contrat. Une présence, c’est un élève, un jour, un booléen. Rien d’autre. Si le jour est dans le futur, on refuse. Si l’élève n’existe pas, on refuse. L’écran n’a pas à inventer une troisième forme.

Le même objet, des deux côtés

Côté Django, le serializer est la porte. Il ne « nettoie » pas en silence. Il dit non.

from datetime import date
from rest_framework import serializers

class PresenceIn(serializers.Serializer):
    eleve_id = serializers.IntegerField(min_value=1)
    jour = serializers.DateField()
    present = serializers.BooleanField()

    def validate_jour(self, value):
        if value > date.today():
            raise serializers.ValidationError(
                "Une présence ne se pose pas dans le futur."
            )
        return value

Côté React, le type reprend les mêmes noms. Pas de studentId d’un côté et eleve_id de l’autre. Le renommage, c’est une deuxième API déguisée.

export type Presence = {
  eleve_id: number;
  jour: string;
  present: boolean;
};

export async function marquerPresence(input: Presence): Promise<Presence> {
  const response = await fetch("/api/presences/", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(input),
  });

  if (!response.ok) {
    const erreur = await response.json();
    throw new Error(erreur.jour?.[0] ?? "Présence refusée.");
  }

  return response.json() as Promise<Presence>;
}

Exemple

Lundi 6 octobre, élève 12, présent. Le corps qui part :

{ "eleve_id": 12, "jour": "2026-10-06", "present": true }

Le même appel avec "jour": "2026-12-01" ne doit pas créer de ligne. La réponse porte le message du serializer, et l’écran l’affiche sous le champ. Pas une alerte générique.

Le contrat JSON entre l’écran React et les règles Django

Les commandes pour le vérifier

Je ne fais pas confiance à l’écran pour me dire que le back tient. Je frappe la vue directement.

curl -s -X POST http://127.0.0.1:8000/api/presences/ \
  -H "Content-Type: application/json" \
  -d '{"eleve_id": 12, "jour": "2026-12-01", "present": true}'

La réponse doit être un 400, avec la phrase sur le futur. Ensuite seulement, je branche le formulaire.

python manage.py shell -c "
from datetime import date
from app.serializers import PresenceIn
donnee = {'eleve_id': 12, 'jour': str(date.today()), 'present': True}
s = PresenceIn(data=donnee)
print(s.is_valid(), s.validated_data if s.is_valid() else s.errors)
"

Quand les deux commandes disent la même chose que le type TypeScript, le contrat est posé. On peut alors changer l’écran sans réécrire les règles, et changer les règles sans casser l’écran en silence.