Skip to content
Learni
Voir tous les tutoriels
E-commerce

Comment implémenter un Headless e-commerce en 2026

Read in English

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

terminal
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:9000

Cette 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

medusa-config.js
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

terminal
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 dev

Cré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

src/lib/medusa.tsx
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

src/app/page.tsx
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

src/hooks/useCart.tsx
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

src/app/checkout/page.tsx
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 tags React 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 : generateStaticParams pour 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 typegen Medusa : 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 swr pour 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.