Ibexa Commerce 5 en headless : construire un catalogue et un panier avec l’API REST
Après avoir migré notre boutique de formation vers Ibexa Commerce 5.0.11, nous voulions répondre à une question très concrète : que signifie réellement “faire du headless” avec Ibexa Commerce ?
Plutôt que d’en rester à une définition abstraite, nous avons construit un petit storefront en HTML, CSS et JavaScript natifs. Il affiche les produits, charge les variantes, crée un panier, recalcule les quantités, applique un code promotionnel et passe ensuite le relais au checkout natif d’Ibexa.
Le résultat est une architecture hybride : catalogue et panier en REST/JSON, puis checkout Symfony sécurisé. C’est une façon pragmatique d’apprendre le headless sans réécrire immédiatement toute la chaîne de commande.
Le code de la démonstration est disponible sur la branche feature/ibexa-5-headless-commerce.
Headless : une définition simple
Dans une application Symfony classique, le serveur prépare les données, Twig construit le HTML, puis le navigateur affiche cette page.
Ibexa Commerce → contrôleur Symfony → template Twig → HTML → navigateur
Dans une architecture headless, le backend ne décide plus de la présentation finale. Il expose des données structurées, généralement en JSON. Une autre application consomme ces données et construit l’interface.
Ibexa Commerce → API REST → JSON → frontend JavaScript → interface
Le mot “headless” ne signifie donc pas “sans interface”. Il signifie que la tête — la couche de présentation — est découplée du moteur de contenu et de commerce.
| Responsabilité | Mode Symfony/Twig | Mode headless de notre REX |
|---|---|---|
| Produits, prix, stocks | Ibexa Commerce | Ibexa Commerce |
| Promotions et calculs | Ibexa Commerce | Ibexa Commerce |
| Production du HTML | Twig côté serveur | JavaScript côté navigateur |
| Transport des données | variables du template | API REST et JSON |
| Checkout | Symfony/Twig | Symfony/Twig après passage de relais |
Le point essentiel est là : nous ne déplaçons pas les règles métier dans JavaScript. Ibexa reste la source de vérité pour le catalogue, les prix, les stocks, les remises et les totaux.
Ce que nous avons construit
La démonstration est disponible localement à l’adresse :
http://127.0.0.1:8003/headless-demo/
Elle est volontairement écrite sans framework frontend. Utiliser du JavaScript natif permet de voir exactement chaque requête et chaque transformation, sans masquer le fonctionnement derrière React, Vue ou Next.js.
La page affiche quatre produits réels du catalogue de formation :
- une formation Ibexa Commerce ;
- un carnet pour développeur ;
- un poster d’architecture ;
- un T-shirt parent décliné en six variantes couleur × taille.
Étape 1 — Interroger un catalogue
Nous filtrons le catalogue demo_catalog avec l’endpoint suivant :
POST /api/ibexa/v2/product/catalog/catalogs/demo_catalog/products/view
Pourquoi un POST pour une opération de lecture ? Parce que la recherche transmet un objet structuré contenant pagination, filtres et tri :
{
"ViewInput": {
"identifier": "headless_catalog",
"ProductQuery": {
"offset": 0,
"limit": 12,
"Filter": {
"ProductAvailabilityCriterion": true
},
"SortClauses": {
"ProductName": "ascending"
}
}
}
}
Les types de médias Ibexa sont également importants :
Accept: application/vnd.ibexa.api.ProductView+json
Content-Type: application/vnd.ibexa.api.ProductViewInput+json
Le frontend reçoit une liste JSON puis fabrique les cartes produit. Aucun produit n’est inscrit en dur dans le HTML initial.
Étape 2 — Charger une fiche, son prix et son stock
Lorsqu’un visiteur ouvre une carte, le navigateur charge la ressource complète :
GET /api/ibexa/v2/product/catalog/products/{code}
La réponse regroupe plusieurs familles de données :
Contentpour le nom, la description, l’image et les champs éditoriaux ;ProductTypepour le type de produit ;Pricepour le prix et la devise ;Availabilitypour la disponibilité et le stock ;codepour l’identifiant commercial stable.
Cette réponse permet aussi de clarifier un point souvent mal compris : le champ ibexa_product_specification transforme un contenu en produit et porte notamment son code, mais le prix et le stock sont gérés séparément par les composants Commerce. L’API rassemble ces données au moment de restituer le produit.
Étape 3 — Distinguer produit parent et variante achetable
Le T-shirt est un produit parent. Il porte les informations communes, mais ce n’est pas le code parent qui est placé dans le panier.
Ses combinaisons sont chargées avec :
POST /api/ibexa/v2/product/catalog/product_variants/view/IBEXA-TSHIRT
Chaque variante possède :
- un code distinct, par exemple
IBEXA-TSHIRT-BLACK-M; - une couleur et une taille ;
- son propre prix ;
- sa propre disponibilité.
Quand l’utilisateur change la sélection, le frontend recharge la variante choisie. Il ne devine pas son prix et ne calcule pas son stock localement.
Étape 4 — Créer un vrai panier REST
Le panier est créé seulement au premier ajout :
POST /api/ibexa/v2/cart
{
"CartCreate": {
"name": "Commerce Lab headless",
"currencyCode": "EUR",
"context": {
"source": "headless-demo"
}
}
}
Ibexa renvoie un UUID. Notre navigateur conserve uniquement cet identifiant dans localStorage. Le contenu commercial du panier reste stocké côté Ibexa.
Pour ajouter la variante noire en taille M :
POST /api/ibexa/v2/cart/{cartIdentifier}/entry
{
"CartEntryAdd": {
"quantity": 1,
"Product": {
"code": "IBEXA-TSHIRT-BLACK-M"
},
"context": {
"source": "headless-demo"
}
}
}
Les quantités et suppressions passent également par l’API :
PATCH /api/ibexa/v2/cart/{cartIdentifier}/entry/{entryIdentifier}
DELETE /api/ibexa/v2/cart/{cartIdentifier}/entry/{entryIdentifier}
Étape 5 — Ne jamais recalculer les totaux dans le frontend
Après chaque modification, nous rechargeons le résumé :
GET /api/ibexa/v2/cart/{cartIdentifier}/summary
Il contient les lignes, les quantités, les prix unitaires, les sous-totaux, les remises, les montants HT et TTC ainsi que la TVA par catégorie.
Cette règle est fondamentale : le navigateur affiche le résultat d’Ibexa, il ne réimplémente pas le moteur de prix. Le calcul dépend potentiellement du catalogue, du client connecté, du groupe client, d’une promotion, d’un code et du contexte de vente.
Étape 6 — Appliquer une promotion au panier
Notre laboratoire possède un code de formation IBEXA15. Il est envoyé à :
POST /api/ibexa/v2/discounts_codes/{cartIdentifier}/apply
{
"DiscountCode": {
"code": "IBEXA15"
}
}
Le frontend recharge ensuite le résumé du panier. Sur un T-shirt à 29,90 € HT, Ibexa retourne une remise de 2,99 € dans le contexte actuel, puis un total de 26,91 € HT et 32,29 € TTC.
Cette capture illustre bien la séparation des responsabilités : le formulaire appartient au frontend, mais l’acceptation du code et le montant de la remise viennent d’Ibexa Commerce.
Étape 7 — Comprendre la protection CSRF
Notre premier appel catalogue a renvoyé :
Missing or invalid CSRF token
Ce n’était pas un bug de l’API. Le navigateur possédait une session Ibexa active et la recherche catalogue utilise POST. Ibexa exige donc un jeton CSRF sur les méthodes non sûres.
Nous avons ajouté un petit endpoint Symfony qui génère le jeton avec l’intention REST :
#[Route('/headless-demo/csrf-token', methods: ['GET'])]
public function csrfToken(CsrfTokenManagerInterface $csrfTokenManager): JsonResponse
{
return $this->json([
'token' => $csrfTokenManager->getToken('rest')->getValue(),
]);
}
Le client le transmet ensuite dans l’en-tête :
X-CSRF-Token: <jeton lié à la session>
Nous avons volontairement conservé la sécurité active. Désactiver CSRF pour faire fonctionner une démonstration aurait masqué un élément essentiel d’une intégration réelle.
Étape 8 — Passer au checkout natif
Dans cette installation, les ressources catalogue et panier sont disponibles via REST. Le parcours complet adresse, livraison, paiement et création de commande reste fourni par le storefront Symfony.
Le bouton final redirige donc vers :
/checkout/init/{cartIdentifier}
Nous obtenons une architecture hybride :
Catalogue headless
→ fiche et variantes REST
→ panier et promotions REST
→ checkout natif Ibexa
→ commande
Ce choix permet de personnaliser fortement la découverte produit sans réécrire immédiatement la partie la plus sensible du tunnel de vente.
Quand cette architecture est-elle utile ?
Le headless devient intéressant lorsqu’un même moteur Commerce doit alimenter plusieurs expériences :
- un site React, Vue ou Next.js ;
- une application mobile ;
- une borne en magasin ;
- un portail B2B ;
- une interface embarquée chez un partenaire.
Il apporte de la liberté frontend, mais ajoute aussi des responsabilités : authentification, CORS, cache, gestion des erreurs, observabilité, SEO et cohérence du panier entre les canaux.
Ce que ce REX prouve — et ce qu’il ne prétend pas faire
Nous avons validé avec Ibexa Commerce 5.0.11 :
- le filtrage d’un catalogue ;
- le chargement des produits et des variantes ;
- la restitution du prix et du stock ;
- la création et la persistance d’un panier ;
- la modification et la suppression des lignes ;
- l’application d’un code promotionnel ;
- le calcul HT, TVA et TTC ;
- le passage vers le checkout ;
- la protection CSRF d’une session REST.
Cette démonstration n’est pas un storefront de production complet. Un projet réellement découplé devrait encore traiter l’authentification, CORS, les performances, le cache, l’accessibilité, le SEO, la gestion avancée des erreurs et éventuellement un checkout entièrement headless.
Conclusion
Le headless n’est pas une suppression de Symfony ou d’Ibexa. C’est une nouvelle répartition des responsabilités.
Ibexa Commerce continue de gérer les données et les règles métier. L’API REST les expose. Le frontend choisit ensuite comment construire l’expérience visible. Dans notre REX, le catalogue et le panier sont découplés, tandis que le checkout natif sécurise la fin du parcours.
Cette première implémentation constitue une base concrète pour aller plus loin : authentification REST, frontend Next.js, cache applicatif, recherche avancée et stratégie omnicanale.


