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.
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.