Introduction
Le headless e-commerce révolutionne le commerce en ligne en découplant le frontend (storefront) du backend (gestion produits, stocks, commandes). Contrairement aux solutions monolithiques comme WooCommerce, un stack headless utilise des APIs REST ou GraphQL pour une flexibilité totale : réutilisez le backend sur mobile, web, IoT.
En 2026, avec Next.js 15 et Medusa.js (open-source, Node.js-based), vous bénéficiez de performances SSR/SSG natives, caching avancé et intégrations Stripe prêtes. Ce tutoriel avancé guide pas à pas la création d'un storefront complet : listing produits, panier persistant, checkout sécurisé.
Pourquoi c'est crucial ? Scalabilité horizontale (backend API scale indépendamment), omnichannel (même API pour app React Native), et SEO boosté via Next.js App Router. À la fin, vous aurez un site e-commerce production-ready, optimisé pour 10k+ produits. Temps estimé : 2h setup + tests.
Prérequis
- Node.js 20+ et pnpm/yarn
- Connaissances avancées en TypeScript, Next.js App Router et GraphQL
- Compte Stripe (test mode) et clé API
- Git et base GitHub pour déploiement Vercel/Netlify
- PostgreSQL local ou Docker pour Medusa (optionnel, SQLite dev OK)
Installer et lancer Medusa backend
npx create-medusa-app@latest my-medusa-store --seed
cd my-medusa-store
pnpm install
pnpm run build
pnpm run start
# Note: Accès admin http://localhost:7001, API http://localhost:9000Cette commande crée un projet Medusa complet avec SQLite dev, seed de données produits/catégories. --seed populate 50+ produits tests. Lancez sur port 9000 (GraphQL/REST) et admin 7001. Piège : utilisez PostgreSQL en prod pour scalabilité ; vérifiez pnpm run typegen post-install pour types TS auto-générés.
Configurer Stripe dans Medusa
Medusa intègre nativement Stripe via plugin. Éditez medusa-config.js pour ajouter paiements. Activez webhooks Stripe pour synchro commandes en temps réel. Testez avec clé pk_test_... et sk_test_....
Configuration Medusa avec Stripe
const DATABASE_URL = "sqlite://localhost/medusa-store.sqlite";
module.exports = {
projectConfig: {
database_url: DATABASE_URL,
database_type: "sqlite",
redis_url: "redis://localhost:6379",
},
plugins: [
`medusa-fulfillment-manual`,
`medusa-payment-stripe`, {
api_key: "sk_test_VOTRE_CLE_SECRETE_STRIPE",
webhook_secret: "whsec_VOTRE_WEBHOOK_SECRET",
},
{
resolve: `@medusajs/admin`,
},
],
};Ce config active Stripe pour paiements + fulfillment manuel. Remplacez clés Stripe (dashboard Stripe > Developers > API keys). Webhook gère events comme payment_intent.succeeded. Piège : en prod, utilisez Redis pour sessions panier ; relancez pnpm run build après modifs.
Créer le storefront Next.js
cd ..
npx create-next-app@15 storefront --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd storefront
pnpm add @medusajs/medusa-js @tanstack/react-query stripe @stripe/stripe-js lucide-react
pnpm add -D @types/node
pnpm run devCrée Next.js 15 avec App Router, Tailwind pour UI rapide. @medusajs/medusa-js est le SDK officiel pour queries produits/panier. React Query pour caching/fetching optimisé. Lance sur localhost:3000. Piège : alignez MEDUSA_BACKEND_URL=http://localhost:9000 dans .env.local.
Intégrer le client Medusa et QueryClient
Créez un provider React Query global avec Medusa client. Cela centralise fetches (produits, variantes, régions). Utilisez useQuery pour SSR/ISR compatibilité.
Provider Medusa et React Query
import Medusa from "@medusajs/medusa-js";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PropsWithChildren } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
},
},
});
export const medusaClient = new Medusa({
baseUrl: process.env.MEDUSA_BACKEND_URL || "http://localhost:9000",
maxRetries: 3,
});
export function Providers({ children }: PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}Client Medusa singleton pour toutes APIs. React Query cache 5min (staleTime) pour perf. maxRetries:3 gère flaky networks. Intégrez dans layout.tsx. Piège : staleTime trop bas = sur-fetches ; testez avec network throttling Chrome DevTools.
Page listing produits avec recherche
import { medusaClient } from "@/lib/medusa";
import { useQuery } from "@tanstack/react-query";
import { Providers } from "@/lib/medusa";
export default function Home() {
const { data: products } = useQuery(
["products"],
() => medusaClient.products().list({ limit: 12 }).then((r) => r.products)
);
return (
<Providers>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-8">
{products?.map((product) => (
<div key={product.id} className="border p-4 rounded-lg">
<img src={product.thumbnail || ""} alt={product.title} className="w-full h-48 object-cover" />
<h3 className="font-bold mt-2">{product.title}</h3>
<p>{product.variants?.[0]?.prices?.[0]?.amount / 100}€</p>
</div>
))}
</div>
</Providers>
);
}Query liste 12 produits avec thumbnail/prix (premier variant/prix). Grid Tailwind responsive. SSR via Next.js fetch auto. Ajoutez search: q param pour filtre. Piège : gérez !products loading/error avec Suspense ; prix /100 car cents Stripe.
Gérer le panier persistant
Analogie : Comme un caddie virtuel localStorage-synced. Utilisez Medusa Cart API pour add/update/remove, avec React Context pour UI state.
Hook panier avec localStorage
import { useCart } from "medusa-react";
import { useEffect } from "react";
export function useManagedCart() {
const { cart, setCart } = useCart();
useEffect(() => {
if (cart?.id) {
localStorage.setItem("cart_id", cart.id);
}
}, [cart?.id]);
const restoreCart = async () => {
const cartId = localStorage.getItem("cart_id");
if (cartId && !cart?.id) {
await setCart(cartId);
}
};
return { cart, restoreCart };
}Hook restaure panier via localStorage (persiste refresh). medusa-react gère mutations auto. Appel restoreCart() dans layout. Piège : expire carts >30j via cron Medusa ; test multi-onglets avec BroadcastChannel si besoin.
Page checkout avec Stripe
import { loadStripe } from "@stripe/stripe-js";
import { medusaClient } from "@/lib/medusa";
import { useCart } from "medusa-react";
import { useState } from "react";
const stripePromise = loadStripe("pk_test_VOTRE_PUB_KEY");
export default function Checkout() {
const { cart } = useCart();
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
const paymentSession = await medusaClient.clients.retrievePaymentSessions(cart!.id);
const stripeSession = paymentSession!.find(s => s.provider_id === "stripe");
const stripe = await stripePromise;
await stripe!.redirectToCheckout({
sessionId: stripeSession!.data!.metadata!.session_id,
});
};
return (
<div className="max-w-md mx-auto p-8">
<h1>Total: {cart?.subtotal / 100}€</h1>
<button
onClick={handleCheckout}
disabled={loading || !cart}
className="w-full bg-blue-500 text-white p-4 mt-4 rounded"
>
Payer avec Stripe
</button>
</div>
);
}Crée session Stripe via Medusa payment_sessions. Redirect to Stripe Elements (no PCI compliance needed). Remplacez PK Stripe. Piège : activez stripe payment provider en admin Medusa ; gérez cart.region pour taxes multi-pays.
Bonnes pratiques
- Caching granulaire : Utilisez
tagsReact Query pour invalider selectivement (ex:invalidateQueries({ queryKey: ['cart'] })post-add). - Régions/taxes : Configurez Medusa regions pour EU VAT auto, query
cart.region_id. - SEO/Perf :
generateStaticParamspour produits SLUG-based, ISR reval 1h. - Sécurité : Validez webhooks Stripe HMAC, rate-limit APIs Medusa.
- Monitoring : Intégrez Sentry pour errors, Prometheus pour metrics backend.
Erreurs courantes à éviter
- Oublier
typegenMedusa : Types TS obsolètes → runtime errors ; relancez post-mods schema. - Panier non-persistant : Sans localStorage/ID restore, abandon rate +50%.
- Stripe en live sans tests : Utilisez toujours test clocks pour simuler temps.
- Pas de fallback offline : Ajoutez
swrpour stale-while-revalidate en PWA.
Pour aller plus loin
Déployez backend Medusa sur Render/DigitalOcean, storefront Vercel. Explorez Medusa modules (emails SendGrid, analytics PostHog).
Découvrez nos formations Learni sur e-commerce headless ou le docs Medusa v2. Rejoignez Discord Medusa pour contribs.