Créer une session de paiement Stripe côté backend
Endpoint sécurisé, création de produit, price dynamique, session checkout, webhooks.
Dans ce chapitre, on va voir comment exposer un endpoint REST dans NestJS pour créer une session de paiement Stripe Checkout. L’objectif : permettre à un utilisateur de faire un don à un autre utilisateur via Stripe, tout en stockant la donation dans Prisma et en transférant automatiquement les fonds sur le compte connecté du bénéficiaire.
Contexte et prérequis
Avant de démarrer, assure-toi d’avoir :
- Un compte Stripe avec ta clé secrète
STRIPE_SECRET_KEYconfigurée en variable d’environnement. - Un module Stripe injecté dans NestJS : un service
StripeServiceet un controllerStripeController. - Un schéma Prisma comportant les entités
UseretDonation. - Un guard JWT activé sur tes routes pour sécuriser l’accès (voir NestJS Guards).
Tip
Pense à valider côté frontend que l’utilisateur courant a un stripeAccountId avant de proposer le bouton “Faire un don”.
1. Définir la route et le controller
On commence par exposer un POST /stripe/donations dans StripeController. Ce controller va recevoir les deux IDs (receivingUserId et givingUserId) et renvoyer l’URL de la session Stripe.
1import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';2import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';3import { StripeService } from './stripe.service';45@Controller('stripe')6export class StripeController {7constructor(private readonly stripeService: StripeService) {}89@UseGuards(JwtAuthGuard) // ^? Sécurisation via JWT10@Post('donations')11async createDonation(12@Body()13payload: {14receivingUserId: string;15},16@Req() req: any,17) {18const givingUserId = req.user.userId;19return this.stripeService.createDonation({20receivingUserId: payload.receivingUserId,21givingUserId,22});23}24}
// @callout: On récupère givingUserId depuis le token JWT fourni par le guard.
2. Implémentation du service de session
Le cœur de la logique se trouve dans StripeService.createDonation(). Voici l’extrait clé :
1async createDonation({2receivingUserId,3givingUserId,4}: {5receivingUserId: string;6givingUserId: string;7}): Promise<{8error: boolean;9message: string;10sessionUrl: string | null;11}> {12try {13if (receivingUserId === givingUserId) {14throw new Error('Vous ne pouvez pas vous faire de dons à vous-même');15}1617// Récupérer les infos Stripe des deux utilisateurs18const [receivingUser, givingUser] = await Promise.all([19this.prisma.user.findUniqueOrThrow({20where: { id: receivingUserId },21select: { id: true, firstName: true, stripeProductId: true, stripeAccountId: true },22}),23this.prisma.user.findUniqueOrThrow({24where: { id: givingUserId },25select: { id: true, stripeAccountId: true },26}),27]);2829if (!receivingUser.stripeAccountId || !givingUser.stripeAccountId) {30throw new Error("Les comptes Stripe doivent être configurés pour les deux utilisateurs");31}
1// Créer le product Stripe si pas encore présent2let { stripeProductId } = receivingUser;3if (!stripeProductId) {4const product = await this.stripe.products.create({5name: `Soutenez ${receivingUser.firstName}`,6});7await this.prisma.user.update({8where: { id: receivingUser.id },9data: { stripeProductId: product.id },10});11stripeProductId = product.id;12}1314// Créer un prix personnalisable15const price = await this.stripe.prices.create({16currency: 'EUR',17custom_unit_amount: { enabled: true },18product: stripeProductId,19});2021▶ // Créer une donation en base pour tracker la session22const createdDonation = await this.prisma.donation.create({23data: {24stripePriceId: price.id,25stripeProductId,26receivingUser: { connect: { id: givingUser.id } },27givingUser: { connect: { id: receivingUser.id } },28},29});3031▶ // Générer la session Checkout32const session = await this.stripe.checkout.sessions.create({33mode: 'payment',34line_items: [{ price: price.id, quantity: 1 }],35payment_intent_data: {36application_fee_amount: 0,37metadata: { donationId: createdDonation.id },38transfer_data: { destination: receivingUser.stripeAccountId },39},40success_url: 'http://localhost:3000',41cancel_url: 'http://localhost:3000',42});4344return {45sessionUrl: session.url,46error: false,47message: 'Session Stripe créée avec succès',48};49} catch (error) {50return { error: true, message: error.message, sessionUrl: null };51}52}
// @callout: custom_unit_amount permet à l’utilisateur de saisir le montant.
2.1 Étapes détaillées
-
Validation
Vérifier que l’utilisateur ne se fait pas de don à lui-même. -
Chargement des comptes Stripe
On récupèrestripeAccountIdetstripeProductIdvia Prisma. -
Création du product
SistripeProductIdest vide, on crée un nouveau produit dans Stripe et on l’enregistre en base. -
Création d’un prix
Chaque don est lié à unPriceStripe aveccustom_unit_amount.enabled. -
Enregistrement de la donation
On crée un enregistrementDonationdans la base pour pouvoir suivre l’avancée. -
Génération de la session Checkout
payment_intent_data.transfer_data.destinationpour router les fonds.metadata.donationIdpour retrouver la donation après webhook.
3. Tester l’endpoint
Utilise curl ou Postman pour déclencher la création de session :
1curl -X POST http://localhost:3000/stripe/donations \2-H "Authorization: Bearer $JWT_TOKEN" \3-H "Content-Type: application/json" \4-d '{"receivingUserId":"usr_123abc"}'
Réponse attendue :
1{2"error": false,3"message": "Session Stripe créée avec succès",4"sessionUrl": "https://checkout.stripe.com/pay/cs_test_…"5}
Ensuite, tu rediriges l’utilisateur vers sessionUrl dans ton UI Remix.
4. Points clés
- On gère la logique de création de produit, prix et session dans le backend pour ne pas exposer la clé secrète Stripe.
custom_unit_amountoffre la flexibilité du montant saisi par l’utilisateur.transfer_data.destinationpermet de verser les dons directement sur le compte Stripe connecté du bénéficiaire.- On stocke en base (Prisma) chaque tentative de donation pour suivi et webhooks.
Sécurité
Ne renvoie jamais ta clé secrète Stripe au frontend. Utilise toujours un endpoint backend.
Exercices rapides
- Validation de montant
Ajoute une règle pour n’accepter que des montants entre 1 € et 500 €. - Affichage dans Remix
Dans une route Remix/donate/$receivingUserId, consomme l’API et crée un bouton “Payer” qui redirige verssessionUrl. - Gestion des erreurs HTTP
Modifie le controller pour renvoyer un statut400sierror: true, plutôt que200.