Créer une session de paiement Stripe côté backend

Endpoint sécurisé, création de produit, price dynamique, session checkout, webhooks.

5 min read

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_KEY configurée en variable d’environnement.
  • Un module Stripe injecté dans NestJS : un service StripeService et un controller StripeController.
  • Un schéma Prisma comportant les entités User et Donation.
  • Un guard JWT activé sur tes routes pour sécuriser l’accès (voir NestJS Guards).

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.

src/stripe/stripe.controller.ts
1
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
2
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
3
import { StripeService } from './stripe.service';
4
5
@Controller('stripe')
6
export class StripeController {
7
constructor(private readonly stripeService: StripeService) {}
8
9
@UseGuards(JwtAuthGuard) // ^? Sécurisation via JWT
10
@Post('donations')
11
async createDonation(
12
@Body()
13
payload: {
14
receivingUserId: string;
15
},
16
@Req() req: any,
17
) {
18
const givingUserId = req.user.userId;
19
return this.stripeService.createDonation({
20
receivingUserId: payload.receivingUserId,
21
givingUserId,
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é :

src/stripe/stripe.service.ts
1
async createDonation({
2
receivingUserId,
3
givingUserId,
4
}: {
5
receivingUserId: string;
6
givingUserId: string;
7
}): Promise<{
8
error: boolean;
9
message: string;
10
sessionUrl: string | null;
11
}> {
12
try {
13
if (receivingUserId === givingUserId) {
14
throw new Error('Vous ne pouvez pas vous faire de dons à vous-même');
15
}
16
17
// Récupérer les infos Stripe des deux utilisateurs
18
const [receivingUser, givingUser] = await Promise.all([
19
this.prisma.user.findUniqueOrThrow({
20
where: { id: receivingUserId },
21
select: { id: true, firstName: true, stripeProductId: true, stripeAccountId: true },
22
}),
23
this.prisma.user.findUniqueOrThrow({
24
where: { id: givingUserId },
25
select: { id: true, stripeAccountId: true },
26
}),
27
]);
28
29
if (!receivingUser.stripeAccountId || !givingUser.stripeAccountId) {
30
throw new Error("Les comptes Stripe doivent être configurés pour les deux utilisateurs");
31
}
{21,31} src/stripe/stripe.service.ts
1
// Créer le product Stripe si pas encore présent
2
let { stripeProductId } = receivingUser;
3
if (!stripeProductId) {
4
const product = await this.stripe.products.create({
5
name: `Soutenez ${receivingUser.firstName}`,
6
});
7
await this.prisma.user.update({
8
where: { id: receivingUser.id },
9
data: { stripeProductId: product.id },
10
});
11
stripeProductId = product.id;
12
}
13
14
// Créer un prix personnalisable
15
const price = await this.stripe.prices.create({
16
currency: 'EUR',
17
custom_unit_amount: { enabled: true },
18
product: stripeProductId,
19
});
20
21
// Créer une donation en base pour tracker la session
22
const createdDonation = await this.prisma.donation.create({
23
data: {
24
stripePriceId: price.id,
25
stripeProductId,
26
receivingUser: { connect: { id: givingUser.id } },
27
givingUser: { connect: { id: receivingUser.id } },
28
},
29
});
30
31
// Générer la session Checkout
32
const session = await this.stripe.checkout.sessions.create({
33
mode: 'payment',
34
line_items: [{ price: price.id, quantity: 1 }],
35
payment_intent_data: {
36
application_fee_amount: 0,
37
metadata: { donationId: createdDonation.id },
38
transfer_data: { destination: receivingUser.stripeAccountId },
39
},
40
success_url: 'http://localhost:3000',
41
cancel_url: 'http://localhost:3000',
42
});
43
44
return {
45
sessionUrl: session.url,
46
error: false,
47
message: 'Session Stripe créée avec succès',
48
};
49
} catch (error) {
50
return { 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

  1. Validation
    Vérifier que l’utilisateur ne se fait pas de don à lui-même.

  2. Chargement des comptes Stripe
    On récupère stripeAccountId et stripeProductId via Prisma.

  3. Création du product
    Si stripeProductId est vide, on crée un nouveau produit dans Stripe et on l’enregistre en base.

  4. Création d’un prix
    Chaque don est lié à un Price Stripe avec custom_unit_amount.enabled.

  5. Enregistrement de la donation
    On crée un enregistrement Donation dans la base pour pouvoir suivre l’avancée.

  6. Génération de la session Checkout

    • payment_intent_data.transfer_data.destination pour router les fonds.
    • metadata.donationId pour retrouver la donation après webhook.

3. Tester l’endpoint

Utilise curl ou Postman pour déclencher la création de session :

Terminal
1
curl -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_amount offre la flexibilité du montant saisi par l’utilisateur.
  • transfer_data.destination permet 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.

Exercices rapides

  1. Validation de montant
    Ajoute une règle pour n’accepter que des montants entre 1 € et 500 €.
  2. Affichage dans Remix
    Dans une route Remix /donate/$receivingUserId, consomme l’API et crée un bouton “Payer” qui redirige vers sessionUrl.
  3. Gestion des erreurs HTTP
    Modifie le controller pour renvoyer un statut 400 si error: true, plutôt que 200.