Skip to Content

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 :

ChampRôle
statuscode HTTP (422, 404…)
titlerésumé (« Validation Failed »)
detaildescription
typeURI décrivant le type d’erreur
violationsliste { 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 ErrorResource cohé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 ?

CodeSignificationExemple
400 Bad Requestrequête malforméeJSON invalide, type incompatible
401 Unauthorizednon authentifiétoken JWT manquant/expiré
403 Forbiddenauthentifié mais pas le droitvoter refuse (chapitre 11.5)
404 Not Foundressource inexistante/api/bookings/999
409 Conflictconflit d’étatdoublon, version concurrente
422 Unprocessable Entitybien formée mais invalidevalidation 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 une ErrorResource personnalisée.
# config/packages/api_platform.yaml (extrait) api_platform: exception_to_status: App\Exception\SlotFullException: 409 # créneau complet → 409 Conflict

Ainsi, 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 mappez violations[].propertyPath sur 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: 409

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