Développement API REST – Au-delà du CRUD

REST est partout – mais la plupart des APIs REST ne sont que des tables de base de données exposées en HTTP. GET, POST, PUT, DELETE mappés sur du CRUD, avec toute la logique métier qui fuit dans les contrôleurs ou, pire, qui est laissée au frontend. Après des années de conception d’APIs web en PHP/Symfony et TypeScript/Node.js, je conçois des APIs qui expriment l’intention métier, appliquent les règles du domaine et guident le client vers les actions disponibles – pas juste servir de la donnée.

REST est orienté CRUD. Votre domaine ne l’est pas.

Le vocabulaire REST standard – GET, POST, PUT, DELETE – se mappe naturellement sur créer/lire/modifier/supprimer. Ça fonctionne pour des ressources simples. Mais les opérations métier réelles rentrent rarement dans « mettre à jour un enregistrement ».

Prenons un système de gestion de commandes. Le client ne « met à jour » pas une commande – il la confirme, l’annule, demande un remboursement, assigne un transporteur. Chacune de ces actions est un use case distinct avec ses propres préconditions, règles métier et effets de bord. Tout cacher derrière un seul PUT /orders/{id}, c’est perdre de l’information – et déléguer la décision au client.

PATCH comme déclencheur de use case

Une approche que j’utilise pour concilier les conventions REST avec la clean architecture : utiliser PATCH pour déclencher des use cases spécifiques. Au lieu d’un « update » générique, chaque requête PATCH cible une opération nommée :

Dispatch de use cases via PATCH PATCH /orders/42/confirm → ConfirmOrderHandler PATCH /orders/42/cancel → CancelOrderHandler PATCH /orders/42/assign-carrier → AssignCarrierHandler

Chaque route est mappée sur un command handler dédié dans la couche applicative. Le contrôleur est un simple adaptateur : il désérialise la requête, construit la commande, la dispatche. Pas de logique métier dans le contrôleur. Pas d’ambiguïté sur l’intention de l’appelant.

Est-ce du REST « pur » ? Les puristes peuvent débattre. Mais REST n’a jamais été une question de pureté – c’est une question de sémantique HTTP pour rendre l’intention explicite. Un PATCH qui modifie une partie de l’état d’une ressource via une opération métier bien définie est plus expressif qu’un PUT qui remplace la représentation entière en espérant que le serveur comprendra ce qui a changé.

CQRS – séparer commandes et requêtes au niveau de l’API

L’approche décrite plus haut mène naturellement au CQRS (Command Query Responsibility Segregation). Quand vos routes d’API distinguent explicitement « ce qui lit » de « ce qui change l’état », l’architecture se clarifie à chaque couche.

Queries – des modèles de lecture optimisés

Les endpoints GET retournent des projections optimisées pour la lecture, pas des dumps d’entités bruts. Le côté query peut utiliser des vues dénormalisées, des index Elasticsearch ou des DTOs dédiés – ce qui sert le mieux le consommateur. Plus de surprises de lazy-loading Doctrine. Plus de requêtes N+1. Le modèle de lecture est conçu pour l’écran, pas pour l’ORM.

Commands – intention explicite, effets de bord explicites

Les endpoints POST et PATCH dispatchent des commandes vers des handlers dédiés. Chaque handler valide les préconditions, applique les règles métier et peut émettre des événements de domaine (commande confirmée, transporteur assigné, facture générée). Le handler est là où le domaine vit – pas le contrôleur, pas l’ORM.

Dans un contexte Symfony, le composant Messenger fournit un command bus naturel. Les commandes sont de simples value objects, les handlers sont des services. Le dispatcher ne se soucie pas de savoir si la commande vient d’une requête HTTP, d’une commande CLI ou d’un message asynchrone – ce qui est exactement le but de la clean architecture.

HATEOAS – l’API dit au client ce qu’il peut faire

L’un des principes les plus sous-utilisés en conception d’API REST est HATEOAS (Hypermedia As The Engine Of Application State). L’idée est simple : la réponse de l’API inclut des liens qui décrivent les actions disponibles en fonction de l’état actuel de la ressource.

Réponse API avec liens hypermedia { "id": 42, "status": "pending", "total": 189.90, "_links": { "self": { "href": "/orders/42" }, "confirm": { "href": "/orders/42/confirm", "method": "PATCH" }, "cancel": { "href": "/orders/42/cancel", "method": "PATCH" } } }

Une fois la commande confirmée, le lien confirm disparaît et de nouveaux liens apparaissent (assign-carrier, request-refund). Le frontend n’a pas besoin de connaître les règles métier qui déterminent les transitions valides – l’API le lui dit.

Pourquoi c’est important pour les équipes front

Sans HATEOAS, le frontend duplique la logique métier : « si le statut est pending et le paiement confirmé et l’utilisateur a le rôle manager, alors afficher le bouton confirmer ». Cette logique est fragile, dispersée dans les composants et dérive des règles backend avec le temps.

Avec les liens HATEOAS, la logique front devient : « si le lien existe, afficher le bouton ». La complexité reste là où elle doit être – dans la couche domaine du backend. Le frontend devient une couche de rendu légère qui suit les instructions de l’API.

C’est particulièrement précieux dans les applications B2B complexes où les règles métier évoluent fréquemment. Un changement de règle côté backend ajuste automatiquement ce que le frontend peut afficher – sans redéployer le front, sans coordonner les releases.

Principes de conception d’API que j’applique

Contract-first

Définir le contrat d’API (OpenAPI / JSON Schema) avant d’écrire du code. Le contrat est le langage partagé entre équipes back et front. Il alimente la génération de code, la documentation et les tests automatisés.

Codes HTTP avec du sens

201 Created pour la création. 409 Conflict pour les violations de règles métier. 422 Unprocessable Entity pour les erreurs de validation. Tout n’est pas un 200 ou un 500.

Stratégie de versioning

Versioning par URL (/v1/) pour les APIs publiques, par header pour les APIs internes. Politiques de dépréciation avec headers sunset et guides de migration – pas de breaking changes silencieux.

Pagination, filtrage, tri

Pagination par curseur pour les gros volumes. Conventions de filtrage cohérentes. Pas de paramètres surprise – chaque filtre documenté dans la spec OpenAPI.

Erreurs comme citoyens de première classe

Payloads d’erreur structurés avec codes machine-readable, messages humains et détails de validation par champ. Le client peut toujours traiter l’erreur programmatiquement, pas juste afficher un message générique.

Sécurité by design

Authentification JWT, flux OAuth2, rate limiting, validation des entrées à la frontière. OWASP API Security Top 10 comme base – pas comme réflexion après coup.

Un mot sur API Platform

API Platform est un outil puissant pour générer des APIs REST et GraphQL à partir d’entités Symfony avec un minimum de code. Pour des APIs CRUD classiques, il délivre vite : pagination, filtres, documentation OpenAPI – tout inclus.

Mais quand votre API doit exprimer des opérations métier au-delà du CRUD – quand vous avez besoin de CQRS, de transitions d’état custom, de workflows pilotés par HATEOAS – les abstractions d’API Platform (State Providers, Processors) commencent à contraindre plus qu’accélérer. L’outil fonctionne au mieux quand le domaine est CRUD-shaped. Quand il ne l’est pas, on passe plus de temps à contourner le framework qu’à coder le domaine.

J’ai utilisé API Platform sur plusieurs projets (notamment chez IAD, où je détaille l’expérience). Mon avis : à utiliser quand le fit CRUD est réel. Pour des APIs orientées use cases avec une logique métier riche, des contrôleurs Symfony standard avec un command bus offrent plus de contrôle avec moins de friction.

Stack technique pour le développement d’API

Clients qui m’ont fait confiance

Sanofi
IAD Immobilier
Leboncoin
Air France

Parlons de votre API

← Retour à l'accueil