API Platform en 2026 : entretien éditorial avec une architecte API Symfony

Choisissez API Platform pour une API Symfony centrée sur des ressources, avec opérations CRUD, validation, pagination, sécurité et contrat OpenAPI standardisés. Préférez une implémentation manuelle lorsque chaque endpoint orchestre un workflow métier atypique, impose un protocole spécifique ou exige un contrôle SQL très fin. GraphQL n'est pas l'option opposée : API Platform peut aussi l'exposer. Une architecte API détaille ici les arbitrages, puis les pièges observés sur les commandes, catalogues et intégrations partenaires.

Entretien éditorial. Choisissez API Platform pour une API Symfony centrée sur des ressources, avec opérations CRUD, validation, pagination, sécurité et contrat OpenAPI standardisés. Préférez une implémentation manuelle lorsque chaque endpoint orchestre un workflow métier atypique, impose un protocole spécifique ou exige un contrôle SQL très fin. GraphQL n’est pas l’option opposée : API Platform peut aussi l’exposer. Une architecte API détaille ici les arbitrages, puis les pièges observés sur les commandes, catalogues et intégrations partenaires.

Architecte API Symfony debout devant un écran affichant un schéma de documentation d'API générée automatiquement
Entretien éditorial : les bons arbitrages entre API Platform et une implémentation manuelle dépendent de la stabilité du domaine métier.

Choisir API Platform ou développer les endpoints à la main

Dans quels projets API Platform apporte-t-il réellement un avantage ?

Il excelle lorsque le domaine peut être présenté sous forme de ressources stables : commandes, produits, factures, comptes ou tickets. On déclare les opérations autorisées, les règles de validation et les groupes de sérialisation ; le framework construit alors une grande partie de la couche HTTP. Cela évite de répéter contrôleurs, réponses d’erreur, pagination et documentation.

Le choix n’est toutefois pas « API Platform contre REST ou GraphQL ». API Platform produit nativement une API REST et peut activer GraphQL. La vraie question est le niveau de convention acceptable.

Situation Choix conseillé Motif
Catalogue avec lecture, filtres et pagination API Platform REST Ressources et opérations prévisibles
Application mobile aux écrans très différents API Platform GraphQL ou REST ciblé Sélection flexible des données
Validation d’une commande avec plusieurs services métier Opération API Platform personnalisée Contrat standard, logique explicite
Import bancaire propriétaire ou webhook complexe Contrôleur Symfony dédié Flux éloigné d’un CRUD
Reporting avec SQL fortement optimisé Provider personnalisé ou endpoint manuel Maîtrise de la requête et du résultat

Pour une commande, je peux conserver une ressource API Platform pour GET /orders/{id}, mais confier POST /orders/{id}/confirm à un processor qui vérifie le stock, réserve le paiement et publie un message. À l’inverse, envelopper artificiellement un import CSV dans une entité Doctrine crée souvent plus de complexité qu’un contrôleur dédié.

L’important est de ne pas exposer automatiquement tout le modèle de persistance. Les DTO d’entrée et de sortie préservent le domaine. Cette séparation rejoint les principes présentés dans le guide complet de Symfony et reste valable si l’équipe hésite encore entre Laravel et Symfony.

Générer OpenAPI sans abandonner la maîtrise du contrat

La documentation automatique est-elle suffisamment fiable pour des clients externes ?

Oui, si les métadonnées sont traitées comme du code produit. La documentation Core d’API Platform confirme que la spécification OpenAPI est générée depuis les ressources et leurs métadonnées PHP, historiquement via annotations et aujourd’hui couramment via attributs. Il n’est donc pas nécessaire d’écrire toute la spécification à la main.

<?php

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    operations: [new Get(), new GetCollection()],
    normalizationContext: ['groups' => ['order:read']]
)]
final class Order
{
    #[Groups(['order:read'])]
    public int $id;

    #[Groups(['order:read'])]
    public string $status;
}

Cette déclaration fournit les chemins, schémas et réponses attendues dans OpenAPI. Mais l’automatisation ne devine ni le vocabulaire métier ni les engagements pris envers les consommateurs. J’ajoute systématiquement des descriptions, des exemples représentatifs et les réponses d’erreur utiles. Pour un partenaire logistique, status doit notamment énumérer les valeurs publiques, pas reproduire aveuglément une constante interne susceptible de changer.

Mon contrôle avant publication comporte trois étapes :

  1. exporter le document OpenAPI dans la CI ;
  2. détecter les changements incompatibles : champ supprimé, type modifié, paramètre devenu obligatoire ;
  3. exécuter des tests de contrat avec un client représentatif.

Une IA peut proposer descriptions et exemples, mais elle ne décide pas si un champ est contractuel. Les techniques de prompt engineering appliqué à PHP et Symfony aident à produire un premier brouillon, jamais à remplacer la revue. Une interface Swagger élégante ne garantit ni la stabilité sémantique ni la bonne gestion des erreurs.

Architecte API présentant un schéma d'architecture sur un écran interactif à un collègue
La documentation OpenAPI générée automatiquement doit être enrichie manuellement pour rester fiable pour des clients externes.

Paginer, filtrer et trier sur de gros volumes

Comment empêcher un endpoint de catalogue ou de commandes de devenir coûteux ?

La documentation officielle de la pagination API Platform décrit deux stratégies natives : pagination par pages et pagination par curseur. La première convient à un back-office où l’utilisateur veut atteindre une page donnée. Le curseur est préférable pour un flux continuellement alimenté, car les insertions entre deux appels perturbent moins la navigation.

<?php

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(
    paginationItemsPerPage: 30,
    paginationClientItemsPerPage: true,
    paginationMaximumItemsPerPage: 100,
    paginationViaCursor: [
        ['field' => 'id', 'direction' => 'DESC']
    ]
)]
final class Order
{
    public int $id;
}

La limite maximale est essentielle : accepter itemsPerPage=100000 transforme une commodité client en risque opérationnel. Pour un historique de commandes, un curseur fondé sur une clé unique et ordonnée évite aussi les ambiguïtés. Si l’ordre métier repose sur createdAt, j’ajoute généralement id comme critère de départage dans la conception de la requête.

Le filtrage exige la même discipline :

  • n’exposer que les propriétés nécessaires aux consommateurs ;
  • indexer les colonnes réellement utilisées dans WHERE et ORDER BY ;
  • éviter les recherches génériques insensibles à la casse sur toutes les colonnes ;
  • refuser les combinaisons de filtres entraînant des jointures incontrôlées ;
  • préférer un filtre métier dédié pour les recherches complexes.

Par exemple, status=paid&customerId=42 peut s’appuyer sur des index adaptés. En revanche, un filtre libre traversant client, adresse, lignes et produit mérite un moteur de recherche ou un provider spécialisé. Pour une exportation complète, je ne désactive pas la pagination : je crée un traitement asynchrone produisant un fichier. Le client reçoit un identifiant de tâche plutôt qu’une réponse HTTP immobilisée pendant l’extraction.

Sécuriser avec JWT, scopes, voters et limitation de débit

JWT suffit-il pour sécuriser une API Platform moderne ?

Non. JWT transporte une identité et des claims ; il ne remplace ni l’autorisation métier, ni la révocation, ni la limitation de débit. La documentation Symfony Security fournit authentification, rôles et voters. API Platform applique ensuite ces décisions à la ressource ou à l’opération. LexikJWTAuthenticationBundle est un choix fréquent pour les jetons JWT, tandis qu’OAuth2 devient pertinent lorsque plusieurs applications, utilisateurs ou scopes doivent être délégués.

<?php

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;

#[ApiResource(operations: [
    new Get(security: "is_granted('ORDER_VIEW', object)"),
    new Post(security: "is_granted('ROLE_ORDER_WRITER')")
])]
final class Order
{
}

Le voter ORDER_VIEW peut vérifier que l’utilisateur appartient au même compte que la commande. C’est plus sûr qu’un simple ROLE_USER, trop large dans une application multitenant. Pour une application partenaire, je traduis aussi les scopes OAuth en permissions explicites : orders:read n’autorise ni remboursement ni modification d’adresse.

Une défense cohérente combine au minimum :

  1. authentification du client ou de l’utilisateur ;
  2. autorisation par opération et, si nécessaire, par objet ;
  3. validation stricte des entrées ;
  4. rate limiting par clé, compte ou adresse selon le scénario ;
  5. journalisation des refus et actions sensibles.

Le composant Rate Limiter de Symfony permet de définir ces politiques. Une route de connexion peut avoir une règle différente d’un endpoint de consultation. Attention également aux proxys : utiliser aveuglément l’adresse transmise dans un en-tête permet de contourner une limite si les proxys de confiance sont mal configurés. Un développement complet doit donc relier la sécurité des API Symfony aux tests d’autorisation et à l’observabilité.

Écran affichant une documentation d'API interactive de type OpenAPI avec une liste d'endpoints colorés par méthode HTTP
Un document OpenAPI exporté en CI permet de détecter les changements incompatibles avant qu'ils n'atteignent les clients de l'API.

Versionner sans casser les applications clientes

Faut-il préférer /v1/, un en-tête Accept ou aucune version visible ?

Aucune stratégie n’est universellement supérieure. Le préfixe /v1/ est lisible, facile à router et pratique pour des partenaires externes. La négociation par Accept garde des URL stables, mais demande une meilleure maîtrise des clients, caches et outils de diagnostic. API Platform permet d’organiser les deux approches par sa configuration et ses opérations.

Je préfère souvent des DTO séparés pour empêcher une évolution du domaine de modifier silencieusement le contrat :

<?php

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;

#[ApiResource(
    uriTemplate: '/v1/orders/{id}',
    operations: [new Get()],
    provider: OrderV1Provider::class
)]
final class OrderV1Output
{
    public int $id;
    public string $status;
}

Une version 2 peut alors remplacer status par un objet détaillé sans altérer la version 1. Le provider traduit le même modèle métier vers deux représentations. Autre cas : ajouter un champ facultatif est généralement compatible, tandis que renommer une propriété ou rendre un paramètre obligatoire impose une transition.

Mon processus est concret : annoncer la dépréciation, mesurer quels clients utilisent encore l’ancienne version, publier une date de retrait adaptée aux engagements contractuels, puis tester les deux contrats pendant la migration. Les en-têtes de dépréciation et une documentation claire facilitent cette période, mais ne remplacent pas le dialogue avec les consommateurs.

Enfin, je lie l’export OpenAPI et les tests de compatibilité au pipeline décrit dans ce guide CI/CD Symfony. Une rupture détectée avant fusion coûte moins cher qu’une application mobile impossible à mettre à jour immédiatement.

Éviter les N+1 et la sérialisation excessive en production

Quels problèmes de performance reviennent le plus souvent ?

Le premier est le N+1 Doctrine. La documentation Performance d’API Platform documente ce risque et recommande notamment un eager loading ciblé ou des dataloaders. Prenons une collection de commandes : une requête charge les commandes, puis l’accès à lines peut déclencher une requête supplémentaire pour chacune.

Pour un écran qui exige réellement les lignes, une requête ciblée peut les joindre :

<?php

final class OrderRepository
{
    public function findRecentWithLines(): array
    {
        return $this->createQueryBuilder('o')
            ->leftJoin('o.lines', 'line')
            ->addSelect('line')
            ->orderBy('o.id', 'DESC')
            ->setMaxResults(30)
            ->getQuery()
            ->getResult();
    }
}

Cette requête doit alimenter un provider dédié ; elle ne doit pas devenir le chargement par défaut de toutes les commandes. Sur l’écran récapitulatif, le nombre de lignes suffit parfois. Charger chaque produit, sa catégorie et ses médias ne ferait que déplacer le problème vers une jointure gigantesque.

Le second piège est la sérialisation. Des groupes trop larges parcourent des graphes d’objets, calculent des accesseurs coûteux et gonflent la réponse. Je contrôle donc séparément :

  • les données récupérées par Doctrine ;
  • les champs exposés par les groupes ;
  • la profondeur des relations ;
  • les calculs déclenchés pendant la normalisation ;
  • la taille finale et la durée observée par endpoint.

Un DTO de liste réduit à id, status, total et customerName est souvent préférable à l’entité complète. Pour le détail, un autre DTO peut inclure les lignes. Ce découpage rend également le contrat OpenAPI plus lisible.

Enfin, les standards PSR-7 et PSR-15 du PHP-FIG définissent des contrats communs pour messages et middlewares HTTP. Symfony utilise son propre modèle HttpFoundation, avec des ponts d’interopérabilité lorsque nécessaire. Cette frontière est utile pour intégrer un middleware ou un composant externe, mais elle ne corrige pas une mauvaise requête Doctrine. En production, je mesure d’abord SQL, mémoire, temps de normalisation et volume transféré ; j’optimise ensuite l’étape réellement responsable.

Questions fréquentes

Quand choisir API Platform plutôt qu'une API Symfony faite main ?
API Platform excelle quand le domaine peut être présenté sous forme de ressources stables : commandes, produits, factures, comptes ou tickets. Pour un import bancaire propriétaire, un webhook complexe ou un reporting SQL très optimisé, un contrôleur Symfony dédié ou un provider personnalisé reste préférable à forcer le CRUD API Platform.
La documentation OpenAPI générée automatiquement est-elle fiable pour des clients externes ?
Oui, si les métadonnées sont traitées comme du code produit : descriptions précises, exemples représentatifs et réponses d'erreur documentées. L'automatisation ne devine ni le vocabulaire métier ni les engagements pris envers les consommateurs, il faut donc enrichir manuellement la spécification générée depuis les attributs PHP.
Pagination par page ou par curseur : laquelle choisir avec API Platform ?
La pagination par page convient à un back-office où l'utilisateur veut atteindre une page précise. La pagination par curseur est préférable pour un flux continuellement alimenté, car les insertions entre deux appels perturbent moins la navigation. Fixer une limite maximale d'éléments par page est indispensable dans les deux cas pour éviter un risque opérationnel.
JWT suffit-il à sécuriser une API Platform en production ?
Non. JWT transporte une identité et des claims mais ne remplace ni l'autorisation métier (via des voters Symfony Security), ni la révocation, ni la limitation de débit. Une défense cohérente combine authentification, autorisation par opération, validation stricte des entrées, rate limiting et journalisation des refus.
Comment éviter les problèmes de performance N+1 avec API Platform et Doctrine ?
En chargeant les relations nécessaires via un eager loading ciblé dans un provider dédié, plutôt qu'en laissant le comportement par défaut déclencher une requête par relation. Il faut aussi contrôler séparément les données récupérées par Doctrine, les champs exposés par les groupes de sérialisation et la profondeur des relations pour éviter une réponse surdimensionnée.