Stripe + Next.js — Guide Complet d'Intégration des Paiements 2026

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Prérequis

Avant de commencer ce tutoriel, assurez-vous d'avoir :

  • Node.js 20+ installé
  • Une expérience avec Next.js 15 (App Router)
  • Un compte Stripe (gratuit à créer sur stripe.com)
  • Des connaissances de base en TypeScript et React
  • Un éditeur de code (VS Code recommandé)

Vous n'avez pas besoin d'une entité commerciale pour tester les paiements — Stripe fournit un mode test complet avec des cartes simulées.

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez un système de paiement complet avec :

  • Paiements uniques via Stripe Checkout
  • Abonnements récurrents avec plusieurs niveaux de tarification
  • Gestion des webhooks pour les événements de paiement en temps réel
  • Portail client pour la gestion des abonnements en libre-service
  • Vérification côté serveur du statut de paiement
  • Routes API type-safe utilisant Next.js App Router

Étape 1 : Configuration du projet

Créez un nouveau projet Next.js et installez les dépendances :

npx create-next-app@latest stripe-payments --typescript --tailwind --app --src-dir
cd stripe-payments

Installez les packages Stripe :

npm install stripe @stripe/stripe-js
  • stripe — SDK Stripe côté serveur pour Node.js
  • @stripe/stripe-js — Chargeur Stripe.js côté client

Étape 2 : Configurer les variables d'environnement

Créez un fichier .env.local à la racine du projet :

# Clés Stripe (utilisez les clés de test pendant le développement)
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
 
# URL de l'application
NEXT_PUBLIC_APP_URL=http://localhost:3000

Trouvez vos clés dans le tableau de bord Stripe sous Developers → API keys. Utilisez toujours les clés préfixées sk_test_ et pk_test_ pour le développement.

Important : N'exposez jamais votre clé secrète côté client. Seules les variables préfixées NEXT_PUBLIC_ sont disponibles dans le navigateur.

Étape 3 : Initialiser le client Stripe

Créez une instance Stripe partagée pour l'utilisation côté serveur :

// src/lib/stripe.ts
import Stripe from "stripe";
 
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-12-18.acacia",
  typescript: true,
});

Créez un chargeur côté client :

// src/lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";
 
let stripePromise: ReturnType<typeof loadStripe>;
 
export function getStripe() {
  if (!stripePromise) {
    stripePromise = loadStripe(
      process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
    );
  }
  return stripePromise;
}

Étape 4 : Créer les produits et les prix dans Stripe

Vous pouvez créer les produits via le tableau de bord Stripe ou par programmation. Pour ce tutoriel, créez-les via le tableau de bord :

  1. Allez dans Products dans votre tableau de bord Stripe
  2. Cliquez sur Add product
  3. Créez un produit appelé "Plan Pro" avec deux prix :
    • Mensuel : 19 $/mois (récurrent)
    • Annuel : 190 $/an (récurrent)
  4. Créez un autre produit appelé "Plan Enterprise" :
    • Mensuel : 49 $/mois (récurrent)
    • Annuel : 490 $/an (récurrent)

Notez les Price IDs (ils commencent par price_). Ajoutez-les à votre environnement :

# Ajoutez à .env.local
STRIPE_PRO_MONTHLY_PRICE_ID=price_...
STRIPE_PRO_YEARLY_PRICE_ID=price_...
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_...
STRIPE_ENTERPRISE_YEARLY_PRICE_ID=price_...

Étape 5 : Construire la page de tarification

Créez un composant de tarification qui affiche vos plans :

// src/app/pricing/page.tsx
import { CheckoutButton } from "@/components/checkout-button";
 
const plans = [
  {
    name: "Pro",
    description: "Pour les équipes en croissance",
    monthlyPrice: 19,
    yearlyPrice: 190,
    monthlyPriceId: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,
    yearlyPriceId: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,
    features: [
      "Projets illimités",
      "Support prioritaire",
      "Analytiques avancées",
      "Accès API",
    ],
  },
  {
    name: "Enterprise",
    description: "Pour les grandes organisations",
    monthlyPrice: 49,
    yearlyPrice: 490,
    monthlyPriceId: process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID!,
    yearlyPriceId: process.env.STRIPE_ENTERPRISE_YEARLY_PRICE_ID!,
    features: [
      "Tout ce qui est dans Pro",
      "Authentification SSO",
      "Intégrations personnalisées",
      "Support dédié",
      "Garantie SLA",
    ],
  },
];
 
export default function PricingPage() {
  return (
    <div className="max-w-4xl mx-auto py-16 px-4">
      <h1 className="text-4xl font-bold text-center mb-4">
        Tarification simple et transparente
      </h1>
      <p className="text-gray-600 text-center mb-12">
        Choisissez le plan qui correspond à vos besoins
      </p>
 
      <div className="grid md:grid-cols-2 gap-8">
        {plans.map((plan) => (
          <div
            key={plan.name}
            className="border rounded-2xl p-8 shadow-sm hover:shadow-md transition"
          >
            <h2 className="text-2xl font-bold">{plan.name}</h2>
            <p className="text-gray-500 mt-2">{plan.description}</p>
            <p className="text-4xl font-bold mt-6">
              {plan.monthlyPrice} $
              <span className="text-base font-normal text-gray-500">
                /mois
              </span>
            </p>
 
            <ul className="mt-6 space-y-3">
              {plan.features.map((feature) => (
                <li key={feature} className="flex items-center gap-2">
                  <span className="text-green-500"></span>
                  {feature}
                </li>
              ))}
            </ul>
 
            <CheckoutButton priceId={plan.monthlyPriceId} planName={plan.name} />
          </div>
        ))}
      </div>
    </div>
  );
}

Étape 6 : Créer la route API de checkout

Construisez la route API côté serveur qui crée une session Stripe Checkout :

// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
 
export async function POST(req: NextRequest) {
  try {
    const { priceId, mode = "subscription" } = await req.json();
 
    if (!priceId) {
      return NextResponse.json(
        { error: "Price ID is required" },
        { status: 400 }
      );
    }
 
    const session = await stripe.checkout.sessions.create({
      mode: mode as "subscription" | "payment",
      payment_method_types: ["card"],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
      billing_address_collection: "required",
      allow_promotion_codes: true,
    });
 
    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error("Erreur checkout:", error);
    return NextResponse.json(
      { error: "Échec de la création de la session" },
      { status: 500 }
    );
  }
}

Étape 7 : Construire le composant bouton de checkout

Créez le composant client qui redirige les utilisateurs vers Stripe Checkout :

// src/components/checkout-button.tsx
"use client";
 
import { useState } from "react";
 
interface CheckoutButtonProps {
  priceId: string;
  planName: string;
}
 
export function CheckoutButton({ priceId, planName }: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false);
 
  async function handleCheckout() {
    setLoading(true);
    try {
      const response = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ priceId }),
      });
 
      const data = await response.json();
 
      if (data.url) {
        window.location.href = data.url;
      } else {
        throw new Error(data.error || "Échec de la création de la session");
      }
    } catch (error) {
      console.error("Erreur checkout:", error);
      alert("Une erreur est survenue. Veuillez réessayer.");
    } finally {
      setLoading(false);
    }
  }
 
  return (
    <button
      onClick={handleCheckout}
      disabled={loading}
      className="w-full mt-8 bg-black text-white py-3 rounded-lg font-medium
                 hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed
                 transition"
    >
      {loading ? "Redirection..." : `S'abonner à ${planName}`}
    </button>
  );
}

Étape 8 : Gérer les webhooks

Les webhooks sont essentiels — ils notifient votre application quand les paiements réussissent, les abonnements se renouvellent ou les paiements échouent. C'est la colonne vertébrale d'un système de paiement fiable.

// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";
 
export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;
 
  let event: Stripe.Event;
 
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Échec de la vérification de la signature:", err);
    return NextResponse.json(
      { error: "Signature invalide" },
      { status: 400 }
    );
  }
 
  try {
    switch (event.type) {
      case "checkout.session.completed": {
        const session = event.data.object as Stripe.Checkout.Session;
        await handleCheckoutComplete(session);
        break;
      }
 
      case "invoice.paid": {
        const invoice = event.data.object as Stripe.Invoice;
        await handleInvoicePaid(invoice);
        break;
      }
 
      case "invoice.payment_failed": {
        const invoice = event.data.object as Stripe.Invoice;
        await handlePaymentFailed(invoice);
        break;
      }
 
      case "customer.subscription.deleted": {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionCancelled(subscription);
        break;
      }
 
      case "customer.subscription.updated": {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionUpdated(subscription);
        break;
      }
 
      default:
        console.log(`Type d'événement non géré : ${event.type}`);
    }
 
    return NextResponse.json({ received: true });
  } catch (error) {
    console.error("Erreur du gestionnaire de webhook:", error);
    return NextResponse.json(
      { error: "Échec du gestionnaire de webhook" },
      { status: 500 }
    );
  }
}
 
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const customerId = session.customer as string;
  const subscriptionId = session.subscription as string;
 
  console.log(
    `Checkout terminé pour le client ${customerId}, abonnement ${subscriptionId}`
  );
 
  // TODO: Sauvegarder dans votre base de données
  // await db.user.update({
  //   where: { stripeCustomerId: customerId },
  //   data: { subscriptionId, subscriptionStatus: "active" },
  // });
}
 
async function handleInvoicePaid(invoice: Stripe.Invoice) {
  const customerId = invoice.customer as string;
  console.log(`Facture payée pour le client ${customerId}`);
 
  // TODO: Mettre à jour la période d'abonnement
}
 
async function handlePaymentFailed(invoice: Stripe.Invoice) {
  const customerId = invoice.customer as string;
  console.log(`Paiement échoué pour le client ${customerId}`);
 
  // TODO: Envoyer un email de notification au client
}
 
async function handleSubscriptionCancelled(subscription: Stripe.Subscription) {
  const customerId = subscription.customer as string;
  console.log(`Abonnement annulé pour le client ${customerId}`);
 
  // TODO: Révoquer l'accès dans votre base de données
}
 
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const customerId = subscription.customer as string;
  const status = subscription.status;
  console.log(
    `Abonnement mis à jour pour le client ${customerId} : ${status}`
  );
 
  // TODO: Mettre à jour les détails du plan
}

Important : La route webhook lit le corps brut en tant que texte (pas JSON) car Stripe a besoin du payload brut pour la vérification de la signature.

Étape 9 : Configurer les tests de webhooks en local

Installez le CLI Stripe pour tester les webhooks localement :

# macOS
brew install stripe/stripe-cli/stripe
 
# Connectez-vous à votre compte Stripe
stripe login
 
# Transférez les webhooks vers votre serveur local
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Le CLI affichera un secret de signature webhook (whsec_...). Copiez-le dans votre .env.local en tant que STRIPE_WEBHOOK_SECRET.

Maintenant, quand vous complétez un checkout de test, les événements seront transférés vers votre gestionnaire de webhooks local.

Étape 10 : Construire la page de succès

Créez une page qui confirme le paiement et affiche les détails de l'abonnement :

// src/app/success/page.tsx
import { stripe } from "@/lib/stripe";
import { redirect } from "next/navigation";
import Link from "next/link";
 
interface SuccessPageProps {
  searchParams: Promise<{ session_id?: string }>;
}
 
export default async function SuccessPage({ searchParams }: SuccessPageProps) {
  const { session_id } = await searchParams;
 
  if (!session_id) {
    redirect("/pricing");
  }
 
  const session = await stripe.checkout.sessions.retrieve(session_id, {
    expand: ["subscription", "line_items"],
  });
 
  if (session.payment_status !== "paid") {
    redirect("/pricing");
  }
 
  const subscription = session.subscription as Stripe.Subscription | null;
 
  return (
    <div className="max-w-lg mx-auto py-16 px-4 text-center">
      <div className="text-6xl mb-6">🎉</div>
      <h1 className="text-3xl font-bold mb-4">Paiement réussi !</h1>
      <p className="text-gray-600 mb-8">
        Merci pour votre abonnement. Votre compte a été mis à niveau.
      </p>
 
      {subscription && (
        <div className="bg-gray-50 rounded-lg p-6 mb-8 text-left">
          <h2 className="font-semibold mb-2">Détails de l'abonnement</h2>
          <p className="text-sm text-gray-600">
            Statut : <span className="capitalize">{subscription.status}</span>
          </p>
          <p className="text-sm text-gray-600">
            Période actuelle se termine le :{" "}
            {new Date(
              subscription.current_period_end * 1000
            ).toLocaleDateString("fr-FR")}
          </p>
        </div>
      )}
 
      <Link
        href="/dashboard"
        className="inline-block bg-black text-white px-6 py-3 rounded-lg
                   hover:bg-gray-800 transition"
      >
        Aller au tableau de bord
      </Link>
    </div>
  );
}

Étape 11 : Ajouter le portail client pour la gestion en libre-service

Le portail client Stripe permet aux utilisateurs de gérer leurs propres abonnements — changer de plan, annuler et mettre à jour les méthodes de paiement.

Configurez d'abord le portail dans votre tableau de bord Stripe sous Settings → Billing → Customer portal.

Puis créez une route API pour générer les sessions du portail :

// src/app/api/portal/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
 
export async function POST(req: NextRequest) {
  try {
    const { customerId } = await req.json();
 
    if (!customerId) {
      return NextResponse.json(
        { error: "Customer ID requis" },
        { status: 400 }
      );
    }
 
    const session = await stripe.billingPortal.sessions.create({
      customer: customerId,
      return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
    });
 
    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error("Erreur portail:", error);
    return NextResponse.json(
      { error: "Échec de la création de la session portail" },
      { status: 500 }
    );
  }
}

Créez un composant bouton pour accéder au portail :

// src/components/manage-subscription-button.tsx
"use client";
 
import { useState } from "react";
 
export function ManageSubscriptionButton({
  customerId,
}: {
  customerId: string;
}) {
  const [loading, setLoading] = useState(false);
 
  async function handleManage() {
    setLoading(true);
    try {
      const response = await fetch("/api/portal", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ customerId }),
      });
 
      const data = await response.json();
      if (data.url) {
        window.location.href = data.url;
      }
    } catch (error) {
      console.error("Erreur portail:", error);
    } finally {
      setLoading(false);
    }
  }
 
  return (
    <button
      onClick={handleManage}
      disabled={loading}
      className="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg
                 hover:bg-gray-200 disabled:opacity-50 transition"
    >
      {loading ? "Chargement..." : "Gérer l'abonnement"}
    </button>
  );
}

Étape 12 : Vérifier le statut de l'abonnement côté serveur

Créez un utilitaire pour vérifier le statut de l'abonnement avant d'accorder l'accès aux fonctionnalités premium :

// src/lib/subscription.ts
import { stripe } from "./stripe";
 
export type SubscriptionStatus =
  | "active"
  | "trialing"
  | "past_due"
  | "cancelled"
  | "none";
 
export async function getSubscriptionStatus(
  customerId: string
): Promise<SubscriptionStatus> {
  try {
    const subscriptions = await stripe.subscriptions.list({
      customer: customerId,
      status: "all",
      limit: 1,
    });
 
    if (subscriptions.data.length === 0) {
      return "none";
    }
 
    const subscription = subscriptions.data[0];
 
    switch (subscription.status) {
      case "active":
        return "active";
      case "trialing":
        return "trialing";
      case "past_due":
        return "past_due";
      case "canceled":
        return "cancelled";
      default:
        return "none";
    }
  } catch (error) {
    console.error("Erreur lors de la récupération de l'abonnement:", error);
    return "none";
  }
}
 
export function hasActiveSubscription(status: SubscriptionStatus): boolean {
  return status === "active" || status === "trialing";
}

Utilisez-le dans vos pages pour protéger le contenu premium :

// src/app/dashboard/page.tsx
import { getSubscriptionStatus, hasActiveSubscription } from "@/lib/subscription";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  // Dans une vraie app, récupérez le customerId depuis votre session auth
  const customerId = "cus_...";
 
  const status = await getSubscriptionStatus(customerId);
 
  if (!hasActiveSubscription(status)) {
    redirect("/pricing");
  }
 
  return (
    <div className="max-w-4xl mx-auto py-16 px-4">
      <h1 className="text-3xl font-bold">Tableau de bord</h1>
      <p className="text-gray-600 mt-2">
        Bienvenue ! Votre abonnement est {status}.
      </p>
      {/* Contenu premium ici */}
    </div>
  );
}

Étape 13 : Gérer les paiements uniques

Tout ne nécessite pas un abonnement. Voici comment gérer les paiements uniques pour des produits numériques ou des services :

// src/app/api/checkout/one-time/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
 
export async function POST(req: NextRequest) {
  try {
    const { productName, amount, currency = "usd" } = await req.json();
 
    const session = await stripe.checkout.sessions.create({
      mode: "payment",
      payment_method_types: ["card"],
      line_items: [
        {
          price_data: {
            currency,
            product_data: {
              name: productName,
            },
            unit_amount: amount, // Montant en cents (ex: 2999 = 29,99 $)
          },
          quantity: 1,
        },
      ],
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    });
 
    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error("Erreur paiement unique:", error);
    return NextResponse.json(
      { error: "Échec de la création du checkout" },
      { status: 500 }
    );
  }
}

Étape 14 : Tester avec les cartes de test Stripe

Stripe fournit des numéros de carte de test pour différents scénarios :

Numéro de carteScénario
4242 4242 4242 4242Paiement réussi
4000 0000 0000 3220Authentification 3D Secure requise
4000 0000 0000 9995Paiement refusé
4000 0000 0000 0341Échec d'attachement de la carte

Utilisez n'importe quelle date d'expiration future, n'importe quel CVC à 3 chiffres et n'importe quel code postal.

Lancez votre application et testez le flux complet :

npm run dev
  1. Visitez http://localhost:3000/pricing
  2. Cliquez sur "S'abonner à Pro"
  3. Utilisez la carte de test 4242 4242 4242 4242
  4. Vérifiez que vous arrivez sur la page de succès
  5. Consultez le tableau de bord Stripe pour le nouvel abonnement

Étape 15 : Déployer le webhook en production

Lors du déploiement, vous devez enregistrer votre URL de webhook de production dans Stripe :

  1. Allez dans Developers → Webhooks dans votre tableau de bord Stripe
  2. Cliquez sur Add endpoint
  3. Entrez votre URL de production : https://votredomaine.com/api/webhooks/stripe
  4. Sélectionnez les événements à écouter :
    • checkout.session.completed
    • invoice.paid
    • invoice.payment_failed
    • customer.subscription.deleted
    • customer.subscription.updated
  5. Copiez le nouveau secret de signature et ajoutez-le à vos variables d'environnement de production

Dépannage

La vérification de la signature webhook échoue

Assurez-vous de lire le corps brut de la requête en tant que texte, pas en le parsant comme JSON avant la vérification. La méthode req.text() de Next.js App Router gère cela correctement.

La session checkout retourne une URL null

Vérifiez que votre clé secrète Stripe est correcte et que le Price ID existe. Les clés du mode test ne fonctionnent qu'avec les prix en mode test.

Le statut de l'abonnement ne se met pas à jour

Assurez-vous que votre endpoint webhook est accessible et retourne des codes de statut 200. Utilisez stripe listen en local ou vérifiez les logs des webhooks dans le tableau de bord Stripe pour les livraisons échouées.

Erreurs CORS lors de la redirection checkout

Vous n'avez pas besoin de headers CORS — Stripe Checkout est une redirection côté serveur, pas un appel API côté client. Assurez-vous d'appeler votre propre route API, pas l'API Stripe directement depuis le navigateur.

Prochaines étapes

  • Ajouter l'authentification — Connectez les clients Stripe à vos comptes utilisateurs avec Auth.js ou Better Auth
  • Stocker les données dans une base — Utilisez Drizzle ORM ou Supabase pour persister les données d'abonnement
  • Ajouter la facturation à l'usage — Facturez en fonction de l'utilisation avec la tarification basée sur l'usage de Stripe
  • Implémenter les périodes d'essai — Offrez des essais gratuits avant de facturer
  • Support multi-devises — Acceptez les paiements dans plusieurs devises pour les clients internationaux

Conclusion

Vous avez construit un système de paiement complet avec Stripe et Next.js qui gère les abonnements, les paiements uniques, les webhooks et la gestion en libre-service via le portail client. L'architecture pilotée par les webhooks garantit que votre application reste synchronisée avec Stripe, peu importe d'où viennent les changements — que ce soit depuis votre application, le tableau de bord Stripe ou le portail client.

Les principes clés à retenir :

  1. Ne faites jamais confiance au client — vérifiez toujours le statut du paiement côté serveur
  2. Les webhooks sont votre source de vérité — ne vous fiez pas uniquement aux callbacks de redirection
  3. Testez minutieusement — utilisez les cartes de test Stripe et le CLI pour simuler chaque scénario
  4. Gérez les échecs avec élégance — les paiements peuvent échouer, les abonnements peuvent expirer, les cartes peuvent être périmées

Avec cette base, vous pouvez étendre votre système de paiement pour gérer n'importe quel modèle de facturation requis par votre application.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Améliorer l'efficacité du service client : Exploiter les appels d'outils obligatoires dans ChatCompletion.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire une API GraphQL typesafe avec Next.js App Router, Yoga et Pothos

Apprenez à construire une API GraphQL entièrement typesafe avec Next.js 15 App Router, GraphQL Yoga et le constructeur de schémas Pothos. Ce tutoriel pratique couvre la conception de schémas, les requêtes, les mutations, le middleware d'authentification et un client React avec urql.

30 min read·

Construire un Starter Kit SaaS avec Next.js 15, Stripe et Auth.js v5

Apprenez a construire une application SaaS prete pour la production avec Next.js 15, Stripe pour la facturation par abonnement, et Auth.js v5 pour l'authentification. Ce tutoriel pas a pas couvre la configuration du projet, la connexion OAuth, les plans tarifaires, la gestion des webhooks et les routes protegees.

35 min read·