How to integrate Stripe in Node.js
Integrating Stripe in Node.js requires the Stripe SDK to create payment intents server-side, a webhook handler to confirm payments asynchronously, and proper error handling for declined cards and API failures.
As the creator of CoreUI with 25 years of backend development experience, I’ve built Stripe integrations for multiple production SaaS and e-commerce platforms.
The most important rule is that all payment logic lives on the server — never expose your secret key or process charges from the frontend.
The server creates a payment intent, sends the client_secret to the frontend, and then receives confirmation via Stripe webhook when payment succeeds.
Install Stripe and initialize the client.
// src/stripe/stripe.client.js
import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16'
})
// src/stripe/payments.router.js
import { Router } from 'express'
import { stripe } from './stripe.client.js'
import { PrismaClient } from '@prisma/client'
const router = Router()
const prisma = new PrismaClient()
Pinning the API version in the constructor ensures your integration doesn’t break when Stripe releases new API versions. Keep the client in a shared module so it’s instantiated once.
Creating a Payment Intent
Generate a payment intent and return the client secret to the frontend.
router.post('/create-intent', async (req, res, next) => {
try {
const { amount, currency = 'usd', customerId } = req.body
const userId = req.user.id
const intentData = {
amount: Math.round(amount * 100), // cents
currency,
automatic_payment_methods: { enabled: true },
metadata: { userId: String(userId) }
}
// Attach to Stripe customer if provided
if (customerId) {
intentData.customer = customerId
}
const intent = await stripe.paymentIntents.create(intentData)
res.json({
clientSecret: intent.client_secret,
intentId: intent.id
})
} catch (err) {
next(err)
}
})
automatic_payment_methods enables all payment methods configured in your Stripe dashboard. Pass amount in cents — Stripe uses the smallest currency unit. The metadata field stores your internal user ID for reference in webhook events.
Creating and Managing Customers
Save customers in Stripe for future payments and subscriptions.
router.post('/customers', async (req, res, next) => {
try {
const { email, name } = req.body
const userId = req.user.id
const customer = await stripe.customers.create({
email,
name,
metadata: { userId: String(userId) }
})
// Save Stripe customer ID in your database
await prisma.user.update({
where: { id: userId },
data: { stripeCustomerId: customer.id }
})
res.json({ customerId: customer.id })
} catch (err) {
next(err)
}
})
Storing the Stripe customer.id in your database links your user to their Stripe customer record, enabling saved payment methods, subscription management, and payment history.
Handling Stripe Webhooks
Receive and verify payment confirmation events.
// Webhook must receive raw body - register BEFORE express.json()
router.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 'payment_intent.succeeded': {
const intent = event.data.object
console.log(`Payment succeeded: ${intent.id}`)
// Fulfill order, send email, etc.
break
}
case 'payment_intent.payment_failed': {
const intent = event.data.object
console.log(`Payment failed: ${intent.last_payment_error?.message}`)
break
}
}
res.json({ received: true })
}
)
stripe.webhooks.constructEvent verifies the stripe-signature header against your webhook secret. If the signature is invalid, it throws — reject the request with 400 to prevent replaying faked events.
Best Practice Note
This is the same server-side Stripe integration pattern used in CoreUI e-commerce and SaaS templates. For subscriptions, use stripe.subscriptions.create() instead of payment intents. For the React frontend that calls these endpoints, see how to integrate Stripe in React. Always test with Stripe test cards (4242 4242 4242 4242) and your webhook via stripe listen --forward-to localhost:3000/stripe/webhook during development.



