Handle Subscription with Node.js and Express using Stripe

In this guide, we will explore how to integrate Stripe payments into a Node.js and Express application. From setting up the Stripe SDK to implementing subscription management, this blog will walk you through the key components.
Setting Up Stripe
To begin, install the Stripe Node.js SDK:
npm install stripe
Next, create a Stripe configuration file to initialize the Stripe SDK with your secret key.
Stripe Configuration
import Stripe from "stripe";
const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) {
throw new Error("STRIPE_SECRET_KEY environment variable is not set");
}
const stripe = new Stripe(stripeKey as string, {
apiVersion: "2024-06-20",
});
export default stripe;
Key Points:
- The
STRIPE_SECRET_KEY
is loaded from environment variables for security. Make sure to have thedotenv
package installed in your project. - The Stripe instance is configured with the latest API version.
Key Features to Implement
1. Cancel Subscription
To allow users to cancel their subscription, use the following function:
export const cancelSubscription = async (req: Request, res: Response) => {
const { userId } = (req as ExtendedRequest).user;
try {
const user = await UserModel.findById(userId);
if (!user) return res.status(404).json({ error: "User not found" });
if (!user.subscription || !user.subscription.customer_id) {
return res.status(400).json({ error: "No active subscription found" });
}
const subscriptions = await stripe.subscriptions.list({
customer: user.subscription.customer_id,
});
if (subscriptions.data.length === 0) {
return res.status(400).json({ error: "No active subscriptions found" });
}
const subscription = subscriptions.data[0];
await stripe.subscriptions.update(subscription.id, { cancel_at_period_end: true });
res.status(200).json({
message: "Subscription cancellation scheduled successfully",
subscription,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
What It Does:
- Fetches the user's active subscriptions.
- Cancels the subscription at the end of the billing period.
2. Check Subscription Status
To allow users to view their subscription details:
export const checkSubscription = async (req: Request, res: Response) => {
const { userId } = (req as ExtendedRequest).user;
try {
const user = await UserModel.findById(userId);
if (!user) return res.status(404).json({ error: "User not found" });
const subscriptions = await stripe.subscriptions.list({
customer: user.subscription.customer_id,
});
if (subscriptions.data.length === 0) {
return res.status(403).json({ error: "No active subscriptions" });
}
const subscription = subscriptions.data[0];
const productId = subscription.plan.product;
const product = await getProductDetailsFromProductId(productId);
const subscriptionData = {
productName: product.name,
price: subscription.plan.amount / 100,
currency: subscription.plan.currency,
cycle: subscription.plan.interval,
status: subscription.status,
};
res.status(200).json({ subscription: subscriptionData });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
What It Does:
- Fetches active subscription details.
- Returns product, price, and subscription status to the user.
3. Start a Free Trial
To enable free trials:
export const startTrial = async (req: Request, res: Response) => {
const { planId } = req.body;
const { userId } = (req as ExtendedRequest).user;
if (!planId) return res.status(400).json({ error: "Missing required fields" });
try {
const prices = await stripe.prices.list({ product: planId });
if (prices.data.length === 0) return res.status(400).json({ error: "No prices found for this product" });
const priceId = prices.data[0].id;
const user = await UserModel.findById(userId);
if (!user || !user.email_verified) {
return res.status(400).json({ error: "Email verification is required to start a trial." });
}
const session = await stripe.checkout.sessions.create({
customer: user.subscription.customer_id,
payment_method_types: ["card"],
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: {
trial_period_days: 7,
},
success_url: `${process.env.CLIENT_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.CLIENT_BASE_URL}/cancel`,
});
res.status(200).json({ url: session.url });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
What It Does:
- Initiates a trial subscription.
- Ensures email verification before proceeding.
4. Subscribe User
For users to purchase a subscription:
export const subscribeUser = async (req: Request, res: Response) => {
const { planId } = req.body;
const { userId } = (req as ExtendedRequest).user;
if (!planId) return res.status(400).json({ error: "Subscription Plan is Required" });
try {
const user = await UserModel.findById(userId);
if (!user || !user.email_verified) {
return res.status(403).json({ error: "Email verification is required to start a subscription." });
}
const prices = await stripe.prices.list({ product: planId });
if (prices.data.length === 0) return res.status(400).json({ error: "No prices found for this product" });
const session = await stripe.checkout.sessions.create({
customer: user.subscription.customer_id,
payment_method_types: ["card"],
mode: "subscription",
line_items: [{ price: prices.data[0].id, quantity: 1 }],
success_url: `${process.env.CLIENT_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.CLIENT_BASE_URL}/cancel`,
});
res.status(200).json({ url: session.url });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
What It Does:
- Processes a new subscription.
- Redirects users to a Stripe-hosted checkout page.
Best Practices
- Environment Variables: Always secure your Stripe keys using environment variables.
- Validation: Ensure proper validation for user input and state.
- Error Handling: Gracefully handle Stripe API errors and provide meaningful responses.
With this setup, you can seamlessly integrate Stripe payments into your Node.js and Express application. Whether it's initiating trials, managing subscriptions, or processing payments, Stripe offers the flexibility to suit various use cases.
That's it for today. See ya 👋