How to Integrate Stripe Payment Gateway in a Web App: Complete Node.js Tutorial

One of the most popular services we offer is ongoing website maintenance because most clients we work with become return clients.

How to Integrate Stripe Payment Gateway in a Web App: Complete Node.js Tutorial

If you’re building a SaaS product, marketplace, or any web app that needs to accept payments, Stripe is the go-to solution for most developers. In this practical tutorial, we’ll walk through how to integrate Stripe in a web app using Node.js on the backend and React on the frontend. We’ll cover API keys, Stripe Checkout, Payment Intents, webhook handling, and testing with the Stripe CLI.

This guide is aimed at developers shipping their first paid product. By the end, you’ll have a working payment flow you can adapt to your own SaaS billing needs.

What You’ll Build

We’ll build a minimal but production-ready payment flow with two integration methods:

  • Stripe Checkout: The fastest way to accept payments with a hosted page.
  • Payment Intents: A customizable flow using Stripe Elements inside your React app.
stripe payment code

Prerequisites

  • Node.js 20+ installed
  • A React 18 or 19 app (Vite or Next.js works)
  • A free Stripe account
  • Basic knowledge of Express and REST APIs

Step 1: Create a Stripe Account and Get Your API Keys

  1. Sign up at stripe.com and complete the basic onboarding.
  2. Go to Developers → API keys in the Stripe Dashboard.
  3. You will see two keys in test mode: a Publishable key (starts with pk_test_) and a Secret key (starts with sk_test_).

Store them in a .env file at the root of your Node project:

STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
CLIENT_URL=http://localhost:5173

Never commit your secret key to Git. Add .env to your .gitignore.

Step 2: Set Up the Node.js Backend

Create a new folder and install the required dependencies:

mkdir stripe-backend && cd stripe-backend
npm init -y
npm install express stripe cors dotenv

Create server.js:

import express from 'express';
import cors from 'cors';
import Stripe from 'stripe';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.use(cors({ origin: process.env.CLIENT_URL }));

// Webhook route needs raw body, register 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) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'checkout.session.completed':
      console.log('Payment successful:', event.data.object.id);
      // TODO: fulfill order, update DB, send email
      break;
    case 'payment_intent.succeeded':
      console.log('PaymentIntent succeeded:', event.data.object.id);
      break;
    default:
      console.log(`Unhandled event: ${event.type}`);
  }

  res.json({ received: true });
});

app.use(express.json());

app.listen(4242, () => console.log('Server running on port 4242'));
stripe payment code

Step 3: Implement Stripe Checkout (The Easy Way)

Stripe Checkout redirects users to a Stripe-hosted page. It’s PCI-compliant out of the box and supports Apple Pay, Google Pay, Link, and dozens of local payment methods. There’s a fuller breakdown if you want the detail.

Add this route to server.js:

app.post('/create-checkout-session', async (req, res) => {
  try {
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items: [{
        price_data: {
          currency: 'usd',
          product_data: { name: 'Pro Plan (One-time)' },
          unit_amount: 2999,
        },
        quantity: 1,
      }],
      success_url: `${process.env.CLIENT_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.CLIENT_URL}/cancel`,
    });
    res.json({ url: session.url });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

On the React side, add a simple checkout button:

function CheckoutButton() {
  const handleClick = async () => {
    const res = await fetch('http://localhost:4242/create-checkout-session', {
      method: 'POST',
    });
    const { url } = await res.json();
    window.location.href = url;
  };
  return <button onClick={handleClick}>Buy Pro Plan</button>;
}

Step 4: Implement Payment Intents with Stripe Elements

If you want the payment form embedded inside your app, use Payment Intents with Stripe Elements. Install the client libraries:

npm install @stripe/stripe-js @stripe/react-stripe-js

Add a Payment Intent endpoint in server.js:

app.post('/create-payment-intent', async (req, res) => {
  const { amount } = req.body;
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'usd',
    automatic_payment_methods: { enabled: true },
  });
  res.json({ clientSecret: paymentIntent.client_secret });
});

Then wire up the React component:

import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { useEffect, useState } from 'react';

const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!stripe || !elements) return;
    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: { return_url: `${window.location.origin}/success` },
    });
    if (error) alert(error.message);
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button disabled={!stripe}>Pay</button>
    </form>
  );
}

export default function Payment() {
  const [clientSecret, setClientSecret] = useState('');
  useEffect(() => {
    fetch('http://localhost:4242/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount: 2999 }),
    })
      .then(r => r.json())
      .then(d => setClientSecret(d.clientSecret));
  }, []);

  return clientSecret ? (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutForm />
    </Elements>
  ) : null;
}

Step 5: Test Webhooks Locally with Stripe CLI

Webhooks are how Stripe tells your server that a payment succeeded. In development, you need the Stripe CLI to forward events to localhost.

  1. Install the CLI from stripe.com/docs/stripe-cli.
  2. Run stripe login and follow the browser flow.
  3. Forward events to your local server:
stripe listen --forward-to localhost:4242/webhook

The CLI will print a webhook signing secret (starts with whsec_). Copy it into your .env as STRIPE_WEBHOOK_SECRET and restart your server. This write-up is worth a look.

Trigger a test event to confirm everything works:

stripe trigger checkout.session.completed

Step 6: Use Stripe Test Cards

Never use real cards in test mode. Stripe provides specific numbers to simulate different scenarios:

Scenario Card Number Result
Successful payment 4242 4242 4242 4242 Payment succeeds
Requires 3D Secure 4000 0025 0000 3155 Authentication required
Declined card 4000 0000 0000 0002 Generic decline
Insufficient funds 4000 0000 0000 9995 Declined for insufficient funds

Use any future expiration date and any 3-digit CVC.

stripe payment code

Checkout vs Payment Intents: Which Should You Choose?

Feature Stripe Checkout Payment Intents + Elements
Setup time Minutes A few hours
UI customization Limited (branding only) Full control
PCI compliance Handled by Stripe Handled by Stripe (SAQ A)
Best for MVPs, simple SaaS Branded checkout, marketplaces

Going to Production: Checklist

  • Activate your Stripe account and switch API keys to live mode.
  • Register a production webhook endpoint in the Dashboard (Developers > Webhooks).
  • Enable HTTPS on your server. Stripe will not send webhooks to plain HTTP.
  • Implement idempotency keys on critical create requests to avoid duplicates.
  • Log webhook events and add retry logic for downstream failures.
  • Store the Stripe customer.id in your database, not raw card data.

Common Mistakes to Avoid

  • Exposing the secret key in client-side code. It must live only on the server.
  • Skipping webhook signature verification. Anyone can POST to your endpoint otherwise.
  • Fulfilling orders on the client success page. Users can close the tab before it loads. Always fulfill in the webhook.
  • Using express.json() before the webhook route. Stripe requires the raw body to verify signatures.

FAQ

Is Stripe integration free?

Stripe has no monthly fee or setup cost. You pay per successful transaction, typically 2.9% + 30 cents in the US, with variations depending on the country and payment method. This is the sort of thing a solid development team ships without fuss.

Do I need a business to use Stripe?

You can start with an individual account for testing, but to accept live payments in most countries you need a registered business or a valid tax ID.

Can I use Stripe with a static HTML site?

Yes, using Payment Links or Stripe Checkout you can accept payments without any backend. However, for subscriptions, custom logic, or fulfillment automation, a Node.js backend is recommended.

How do I handle recurring subscriptions?

Use Stripe Billing. Create a Product and a recurring Price in the Dashboard, then pass mode: 'subscription' when creating the Checkout Session. Handle events like invoice.paid and customer.subscription.deleted via webhooks.

What’s the difference between Payment Intents and Charges API?

The Charges API is legacy. Payment Intents is the current standard and handles Strong Customer Authentication (SCA), 3D Secure, and asynchronous payment methods natively. Always use Payment Intents for new integrations.

How long does Stripe take to pay out?

The first payout usually takes 7 to 14 days depending on your country. After that, payouts run on a rolling 2-day schedule by default, and can be adjusted in the Dashboard. There’s a good explainer over at stripe.com.

Wrapping Up

You now have a working Stripe integration in your Node.js and React web app, complete with Checkout, Payment Intents, and secure webhook handling. Start in test mode, iterate on your flow, then flip the switch to live keys when you’re ready to charge real customers.

At Pixelseed, we help startups ship production-grade SaaS billing flows with Stripe. If you’d like a hand setting up subscriptions, usage-based pricing, or Connect for marketplaces, get in touch.

Subscription Form

Contact Details

Quick Links

Copyright © 2022 Pixel Seed. All Rights Reserved.