Paiements Stripe : créer la commande avant Checkout

Apprends à générer une commande fiable avec React Router 7 : validation Zod, décrémentation stock, création Order Prisma puis redirection vers Checkout Stripe.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
72 leçons
Accès à vie
299.49
-35%

Création de la commande côté serveur

Nous terminons l’intégration Stripe : avant de lancer le checkout, il nous faut d’abord créer une vraie commande dans la base. Objectif de cette étape :

  1. ajouter un service orders.server.ts ;
  2. valider le panier (produits, stock, user ou guest) avec Zod ;
  3. décrémenter le stock ;
  4. renvoyer l’orderId pour la redirection.

Dossier customer et fichier orders.server.ts

app/server/customer/orders.server.ts
1
import type { Prisma } from "generated/prisma/client";
2
import { prisma } from "~/server/db.server";
3
4
/* ----------------------------------------------------------- */
5
/* helper – vérifie stock + construit chaque ligne de commande */
6
/* ----------------------------------------------------------- */
7
async function makeOrderItems(
8
cartItems: { productId: string; quantity: number }[],
9
) {
10
const ids = cartItems.map((i) => i.productId);
11
const products = await prisma.product.findMany({
12
where: { id: { in: ids }, isActive: true },
13
});
14
15
if (products.length !== ids.length) {
16
throw new Error("Product missing or inactive");
17
}
18
19
let subtotal = 0;
20
const items: Prisma.OrderItemCreateWithoutOrderInput[] = [];
21
22
for (const ci of cartItems) {
23
const p = products.find((pr) => pr.id === ci.productId)!;
24
25
if (p.stock < ci.quantity) {
26
throw new Error(`Stock insuffisant pour ${p.name}`);
27
}
28
29
subtotal += p.priceCents * ci.quantity;
30
31
items.push({
32
productId: p.id,
33
productName: p.name,
34
quantity: ci.quantity,
35
unitPriceCents: p.priceCents,
36
totalPriceCents: p.priceCents * ci.quantity,
37
stripePriceId: p.stripePriceId,
38
});
39
}
40
41
return { items, subtotal };
42
}
43
44
/* ----------------------------- */
45
/* API principale : createOrder */
46
/* ----------------------------- */
47
export async function createOrder({
48
cartItems,
49
userId,
50
guestEmail,
51
}: {
52
cartItems: { productId: string; quantity: number }[];
53
userId?: string;
54
guestEmail?: string;
55
}) {
56
if (!cartItems.length) throw new Error("Panier vide");
57
58
if (!userId && !guestEmail) {
59
throw new Error("userId ou guestEmail requis");
60
}
61
62
const { items, subtotal } = await makeOrderItems(cartItems);
63
64
const order = await prisma.order.create({
65
data: {
66
userId,
67
guestEmail,
68
subtotalCents: subtotal,
69
totalCents: subtotal, // pas de taxe/livraison pour l’instant
70
currency: "EUR",
71
orderStatus: "DRAFT",
72
paymentStatus: "PENDING",
73
deliveryStatus: "PENDING",
74
items: { create: items },
75
},
76
});
77
78
/* décrémente le stock */
79
await Promise.all(
80
items.map((it) =>
81
prisma.product.update({
82
where: { id: it.productId! },
83
data: { stock: { decrement: it.quantity } },
84
}),
85
),
86
);
87
88
return order;
89
}

Pourquoi ?

  • stock et activité du produit sont vérifiés avant la création ;
  • on garde le prix du moment — si tu modifies le tarif demain, l’historique ne change pas ;
  • la décrémentation est atomique grâce à Prisma.

Validation du formulaire panier

Schéma Zod côté client

app/routes/_public+/cart.tsx
1
import { z } from "zod";
2
3
export const CreateOrderSchema = z.object({
4
intent: z.literal("create-order"),
5
items: z
6
.array(
7
z.object({
8
productId: z.string(),
9
quantity: z.number().int().positive(),
10
}),
11
)
12
.min(1, "Le panier ne peut pas être vide"),
13
guestEmail: z.string().email().optional(),
14
});

Utilisation dans l’action Remix

app/routes/_public+/cart.tsx
1
import { badRequest, parseWithZod } from "@conform-to/zod";
2
import { createOrder } from "~/server/customer/orders.server";
3
import { getOptionalUser } from "~/server/auth.server";
4
5
export async function action({ request }: ActionFunctionArgs) {
6
const fd = await request.formData();
7
const submission = parseWithZod(fd, { schema: CreateOrderSchema });
8
9
if (submission.status !== "success") {
10
return badRequest(submission.reply());
11
}
12
13
const user = await getOptionalUser(request);
14
15
/* création effective */
16
const order = await createOrder({
17
cartItems: submission.value.items,
18
userId: user?.id,
19
guestEmail: submission.value.guestEmail,
20
});
21
22
/* redirection + flush cookie panier */
23
return redirect(`/orders/${order.id}?success=true`);
24
}

Hook useCart : ne stocker que l’identifiant + la quantité

Au lieu de garder la copie complète du produit dans localStorage, on enregistre uniquement { productId, quantity }. En SSR, la route root charge la version « fraîche » depuis la base, garantissant que le nom, le prix et le stock sont toujours à jour.

app/hooks/use-cart.ts {16,33,68}
1
type LSItem = { productId: string; quantity: number };
2
3
const [rawCart, setRawCart] = useLocalStorage<LSItem[]>("cart", []);
4
5
const products = useLoaderData<typeof rootLoader>().products; // infos fraîches
6
7
const cart = useMemo(() => {
8
return rawCart
9
.map((rc) => ({
10
product: products.find((p) => p.id === rc.productId)!,
11
quantity: rc.quantity,
12
}))
13
.filter((ci) => ci.product); // produit supprimé => item ignoré
14
}, [rawCart, products]);

Vider le panier après commande

app/routes/_public+/cart.tsx
1
import { useSearchParams } from "react-router";
2
import { useEffect } from "react";
3
import { useCartContext } from "~/contexts/cart-context";
4
5
export default function CartRoute() {
6
const { clearCart } = useCartContext();
7
const [sp, setSp] = useSearchParams();
8
9
useEffect(() => {
10
if (sp.get("success") === "true") {
11
clearCart();
12
sp.delete("success");
13
setSp(sp, { replace: true });
14
}
15
}, []);
16
/* … */
17
}

Mise à jour UI : bouton « Commander maintenant »

app/components/OrderSummary.tsx
1
<Form method="POST" {...getFormProps(form)} className="space-y-4">
2
<input type="hidden" name="intent" value="create-order" />
3
<input
4
type="hidden"
5
name="items"
6
value={JSON.stringify(cart.items.map(({ product, quantity }) => ({
7
productId: product.id,
8
quantity,
9
})))}
10
/>
11
<Button
12
type="submit"
13
className="w-full"
14
isLoading={navigation.state === "submitting"}
15
>
16
Commander maintenant
17
</Button>
18
</Form>

Tests rapides

  1. ajoute 3 produits au panier ;
  2. modifie le prix d’un produit dans l’admin, sans recharger la page panier ;
  3. recharge la page → le prix affiché est bien le nouveau ;
  4. clique « Commander » → redirection /orders/:id puis panier vidé.

À retenir

  • le panier persistant ne stocke que l’ID + quantité ; le reste vient du serveur (pas de données périmées) ;
  • createOrder se charge du stock et fixe le prix au moment T ;
  • validation Zod + Conform = un seul schéma partagé client / serveur ;
  • redirection avec ?success=true permet de vider le panier côté client ;
  • prochaine étape : créer la session Checkout Stripe puis écouter le webhook checkout.session.completed.

Ton e-commerce est maintenant capable de générer une commande fiable ; on attaque le paiement dans la prochaine leçon !

Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours