If you are building a modern SaaS, e-commerce store or marketplace, chances are you need a reliable payment flow. In this tutorial, we walk through a complete Stripe integration in Node.js and React, including Stripe Checkout, webhook handling, error management and testing in sandbox mode. Unlike most tutorials that stop at the happy path, we cover what actually breaks in production and how to fix it.
Why Stripe with Node.js and React?
Stripe remains the go-to payment processor for developers in 2026 thanks to its clean API, strong documentation and PCI compliance handled out of the box. Combining it with a Node.js backend and a React frontend gives you:
- A secure server-side environment for secret keys and webhook verification
- A reactive, modern UI on the frontend with @stripe/react-stripe-js
- Easy deployment on Vercel, Render, Railway or AWS
- Built-in support for SCA, 3D Secure and global payment methods

What You Will Build
A working checkout flow where a user clicks a Buy button in React, gets redirected to Stripe Checkout, completes the payment in sandbox mode, and your Node.js backend receives a verified webhook to fulfill the order.
Tech Stack
| Layer | Tool | Version (2026) |
|---|---|---|
| Frontend | React + Vite | React 19 |
| Backend | Node.js + Express | Node 22 LTS |
| Payments | Stripe SDK | stripe@^17 |
| Frontend SDK | @stripe/stripe-js | ^4 |
Step 1: Create Your Stripe Account and Get API Keys
- Sign up at stripe.com and stay in Test mode
- Go to Developers > API keys
- Copy your Publishable key (pk_test_…) and Secret key (sk_test_…)
- Never commit the secret key. Put it in a
.envfile
Step 2: Set Up the Node.js Backend
Create a backend folder and install dependencies:
mkdir server && cd server
npm init -y
npm install express stripe cors dotenv
Create server.js:
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();
app.use(cors({ origin: 'http://localhost:5173' }));
// Webhook needs raw body, so we declare it BEFORE express.json()
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook signature failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
console.log('Payment succeeded for session', session.id);
// TODO: fulfill order in your DB
break;
}
case 'payment_intent.payment_failed':
console.log('Payment failed');
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
res.json({ received: true });
});
app.use(express.json());
app.post('/create-checkout-session', async (req, res) => {
try {
const { items } = req.body;
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: items.map(i => ({
price_data: {
currency: 'eur',
product_data: { name: i.name },
unit_amount: i.amount
},
quantity: i.quantity
})),
success_url: 'http://localhost:5173/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'http://localhost:5173/cancel'
});
res.json({ url: session.url });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
app.listen(4242, () => console.log('Server on http://localhost:4242'));
Your .env should look like:
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

Step 3: Build the React Frontend
Create a Vite app and install the Stripe.js loader:
npm create vite@latest client -- --template react
cd client
npm install @stripe/stripe-js
Create Checkout.jsx:
import { loadStripe } from '@stripe/stripe-js';
import { useState } from 'react';
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PK);
export default function Checkout() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleCheckout = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('http://localhost:4242/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ name: 'Pro Plan', amount: 2999, quantity: 1 }]
})
});
if (!res.ok) throw new Error('Failed to create session');
const { url } = await res.json();
window.location.href = url;
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handleCheckout} disabled={loading}>
{loading ? 'Redirecting...' : 'Buy Pro Plan - 29.99 EUR'}
</button>
{error && <p style={{color:'red'}}>{error}</p>}
</div>
);
}
Add your publishable key to client/.env:
VITE_STRIPE_PK=pk_test_xxx
Step 4: Test Webhooks Locally with the Stripe CLI
- Install the Stripe CLI from stripe.com/docs/stripe-cli
- Login:
stripe login - Forward webhooks to your local server:
stripe listen --forward-to localhost:4242/webhook - Copy the whsec_… printed in your terminal and paste it into your
.env - Restart your Node server
Step 5: Run a Full Test in Sandbox Mode
Use one of Stripe’s test cards in Checkout:
| Scenario | Card Number | Result |
|---|---|---|
| Successful payment | 4242 4242 4242 4242 | Payment succeeds |
| Requires 3D Secure | 4000 0027 6000 3184 | Triggers authentication |
| Card declined | 4000 0000 0000 9995 | Insufficient funds |
| Generic decline | 4000 0000 0000 0002 | Card declined |
Use any future expiry, any 3-digit CVC and any postal code.

Error Handling Best Practices
- Never trust the client: always recompute prices on the server using product IDs or a database lookup
- Verify webhook signatures: a missing or wrong
STRIPE_WEBHOOK_SECRETis the #1 cause of broken integrations - Idempotency: webhooks can be delivered more than once. Store the
event.idand skip duplicates - Use HTTP 2xx quickly: respond to Stripe within a few seconds, then handle heavy work asynchronously
- Log everything: keep a record of
session.id,payment_intent.idand customer email for support
Going to Production
- Activate your Stripe account and switch to live API keys
- Create a real webhook endpoint in Developers > Webhooks pointing to your production URL
- Set
success_urlandcancel_urlto your live domain - Enable additional payment methods like Apple Pay, Google Pay, SEPA or Klarna from the dashboard
- Add monitoring with Sentry or Datadog on both backend errors and webhook failures
How Our Approach Differs
Most tutorials show you a happy-path integration that breaks the moment a webhook fails or a card is declined. This guide gives you a production-ready blueprint: signed webhook verification, idempotency, error handling on the React side, and a real testing matrix. Copy the snippets, plug in your keys, and you have a working Stripe checkout in under 30 minutes.
FAQ
Do I need a backend for Stripe with React?
Yes. The Stripe secret key must never live in the browser. A Node.js backend creates the Checkout Session or PaymentIntent and verifies webhooks.
Should I use Stripe Checkout or Stripe Elements?
Use Checkout for the fastest setup and a Stripe-hosted payment page. Use Elements with @stripe/react-stripe-js when you need a fully custom UI inside your app.
How do I test Stripe locally?
Use the Stripe CLI with stripe listen --forward-to localhost:4242/webhook and the test card 4242 4242 4242 4242.
How do I handle failed payments?
Listen for payment_intent.payment_failed and checkout.session.expired webhook events, then notify the user by email or retry logic.
Is Stripe free to use?
Stripe takes a per-transaction fee (typically around 1.5% + 0.25 EUR in Europe in 2026). There is no monthly fee for the standard plan.
Can I use this setup with Next.js instead of plain React?
Yes. Replace Express routes with Next.js API routes or Route Handlers. The Stripe SDK calls remain identical.
With this foundation in place, you have a robust Stripe integration with Node.js and React ready to scale. Bookmark this guide and reuse the snippets for your next SaaS or e-commerce project.

