Chapitre 11.4 — Validation & erreurs
Partie 11 : API Platform — Chapitre 4/8 Version de référence : API Platform 4 / Symfony 7.4.
⏱️ TL;DR
- API Platform réutilise vos contraintes
#[Assert\...](Partie 9) : une entrée invalide renvoie automatiquement un 422 détaillé, sans code.- Le format d’erreur suit RFC 7807 (
application/problem+json) :type,title,status,detail, et la liste des violations (champ + message).- On active des groupes de validation par opération (règles différentes création/édition).
- Les exceptions métier peuvent être mappées à un statut HTTP ; on personnalise les réponses d’erreur.
- 400 = requête malformée (JSON invalide) ; 422 = requête bien formée mais invalide métier ; 404/403/409 selon le cas.
- Pour un dev Next.js : le front lit le statut (pour brancher sa logique) et les violations (pour afficher les erreurs par champ) — un contrat d’erreur standard et prévisible.
🎯 Objectifs
- Comprendre la validation automatique et le format d’erreur.
- Utiliser des groupes de validation par opération.
- Mapper une exception métier à un statut HTTP.
- Distinguer les codes d’erreur (400 vs 422 vs 409).
1. La validation, gratuite et partagée
Les contraintes que vous avez écrites (Partie 9) sur vos DTO/entités s’appliquent automatiquement aux opérations d’écriture de l’API. Une réservation avec un email invalide :
POST /api/bookings { "customerEmail": "pas-un-email", "start": "2020-01-01T10:00:00Z" }renvoie, sans une ligne de code, un 422 :
{
"@type": "ConstraintViolationList",
"status": 422,
"violations": [
{ "propertyPath": "customerEmail", "message": "Email invalide." },
{ "propertyPath": "start", "message": "La date doit être dans le futur." }
]
}C’est la même validation qui protège le formulaire du back-office : une déclaration, deux portes (formulaire HTML + API JSON). C’est tout l’intérêt d’avoir centralisé les contraintes sur des DTO (Partie 9.2).
2. Le format d’erreur (RFC 7807)
API Platform normalise les erreurs selon RFC 7807 (application/problem+json) — un standard que les clients connaissent :
| Champ | Rôle |
|---|---|
status | code HTTP (422, 404…) |
title | résumé (« Validation Failed ») |
detail | description |
type | URI décrivant le type d’erreur |
violations | liste { propertyPath, message } (pour la validation) |
Un client Next.js peut donc, de façon uniforme, lire status (que faire ?) et violations (afficher les messages sous chaque champ). Le format ne change pas d’un endpoint à l’autre.
⚡ Depuis Symfony 5 : API Platform 3.2+ a unifié la gestion des erreurs autour de RFC 7807 et d’une
ErrorResourcecohérente. Si vous aviez des souvenirs de formats d’erreur hétérogènes, c’est désormais standardisé — plus facile à consommer côté front.
3. Groupes de validation par opération
Comme pour les formulaires, on active des groupes selon l’opération :
use ApiPlatform\Metadata\{Post, Patch};
#[ApiResource(operations: [
new Post(validationContext: ['groups' => ['Default', 'create']]),
new Patch(validationContext: ['groups' => ['Default', 'update']]),
])]
class Booking { /* ... */ }Ainsi, une contrainte #[Assert\NotBlank(groups: ['create'])] ne s’applique qu’à la création. Utile quand création et mise à jour ont des règles différentes.
4. Codes d’erreur : lequel quand ?
| Code | Signification | Exemple |
|---|---|---|
| 400 Bad Request | requête malformée | JSON invalide, type incompatible |
| 401 Unauthorized | non authentifié | token JWT manquant/expiré |
| 403 Forbidden | authentifié mais pas le droit | voter refuse (chapitre 11.5) |
| 404 Not Found | ressource inexistante | /api/bookings/999 |
| 409 Conflict | conflit d’état | doublon, version concurrente |
| 422 Unprocessable Entity | bien formée mais invalide | validation métier échoue |
⚠️ Piège 400 vs 422 : ne renvoyez pas 400 pour une erreur de validation métier. 400 = « je ne comprends même pas ta requête » (JSON cassé, mauvais type). 422 = « je comprends, mais les données sont invalides » (email mal formé, date passée). Le front en tire des logiques différentes (400 = bug d’appel ; 422 = afficher les erreurs de champ). API Platform choisit correctement par défaut ; respectez cette sémantique dans vos erreurs custom.
5. Mapper une exception métier à un statut
Vos exceptions métier (Partie 2.7) peuvent produire des réponses propres. Deux approches :
- Lever une exception HTTP de Symfony (
ConflictHttpException, etc.) → statut correspondant. - Marquer une exception métier avec son statut via la configuration d’API Platform (
exception_to_status) ou uneErrorResourcepersonnalisée.
# config/packages/api_platform.yaml (extrait)
api_platform:
exception_to_status:
App\Exception\SlotFullException: 409 # créneau complet → 409 ConflictAinsi, quand votre service lève SlotFullException (au sein d’une opération), l’API répond 409 avec un message propre, au lieu d’une 500.
6. Validation métier via contrainte custom
La règle « le créneau doit être disponible » (contrainte custom, Partie 9.3) s’applique aussi à l’API : posée sur le DTO d’entrée, elle refuse une réservation sur un créneau complet avec un 422 clair — sans dupliquer la logique. C’est le bénéfice de contraintes partagées entre formulaire et API.
💡 Pour un dev Next.js : côté front, vous traitez
res.status. Sur 422, vous mappezviolations[].propertyPathsur vos champs de formulaire (react-hook-form :setError(path, message)). Sur 401, vous tentez un refresh (Partie 10.4). Sur 403, vous cachez/refusez l’action. Comme le format est standard (RFC 7807), ce mapping se code une fois et sert partout.
✏️ Exercices
Exercice 1. Quel code HTTP pour : (a) JSON du corps invalide ; (b) email mal formé ; (c) créneau complet ; (d) token expiré ; (e) réservation inexistante ?
✅ Solution
(a) 400 (Bad Request, requête malformée). (b) 422 (Unprocessable Entity, validation). (c) 409 (Conflict, conflit d’état) — ou 422 selon la modélisation. (d) 401 (Unauthorized). (e) 404 (Not Found).
Exercice 2. Comment faire pour qu’une SlotFullException levée pendant une création renvoie 409 au lieu de 500 ?
✅ Solution
La mapper dans la config API Platform :
api_platform:
exception_to_status:
App\Exception\SlotFullException: 409Ainsi, quand le service lève SlotFullException au sein de l’opération, l’API répond 409 Conflict avec un message propre. (Alternative : lever directement ConflictHttpException, ou utiliser une contrainte de validation → 422.)
Exercice 3. Pourquoi la validation n’a-t-elle pas besoin d’être réécrite pour l’API alors qu’on l’a écrite pour les formulaires ?
✅ Solution
Parce que les contraintes #[Assert\...] sont posées sur les DTO/entités, pas sur les formulaires. Le composant Validator est utilisé à la fois par les formulaires (isValid()) et par API Platform (validation automatique des opérations d’écriture). Une déclaration protège donc les deux entrées (HTML et JSON) — sans duplication.
🧠 Quiz de révision
1. Que se passe-t-il quand l’entrée d’une opération d’écriture est invalide ?
API Platform renvoie automatiquement un 422 avec la liste des violations (champ + message), en réutilisant vos contraintes #[Assert\...].
2. Quel standard suit le format d’erreur ?
RFC 7807 (application/problem+json) : status, title, detail, type, et violations pour la validation.
3. Différence entre 400 et 422 ?
400 = requête malformée (JSON cassé, mauvais type) ; 422 = requête bien formée mais invalide métier (validation).
4. Comment mapper une exception métier à un code HTTP ?
Via exception_to_status dans la config API Platform (ou en levant une exception HTTP Symfony / une ErrorResource).
5. Comment le front exploite-t-il un 422 ?
Il lit violations[].propertyPath/message et affiche les erreurs sous chaque champ (mapping vers le formulaire) ; le format standard rend ce traitement générique.
Prochain chapitre : 11.5 — Sécurité de l’API — protéger chaque opération avec JWT et voters.