As e-commerce continues to boom, integrating reliable payment systems into web applications has become crucial. Stripe, known for its developer-friendly approach, pairs exceptionally well with Next.js, a powerful React framework. In this guide, we’ll walk through the process of integrating Stripe into a Next.js application, sharing insights and best practices along the way. In this tutorial we will implement how to integrate Stripe in Nextjs and at the end of the tutorial i will share you github code for an example.
Table of Contents
1. Setting Up Your Stripe Nextjs 14 Project
Before we dive into Stripe integration, ensure you have a Next.js project ready. If you’re starting from scratch, create a new project:
npx create-next-app@latest my-stripe-app
cd my-stripe-app
We also need to install packages related to stripe library.
npm install stripe @stripe/stripe-js @stripe/react-stripe-js axios
For the typescript dependency let install
npm i @types/stripe
2. Configuring Environment Variables
Security is paramount when dealing with payments. Create a .env.local
file in your project root and add your Stripe keys:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key_here
STRIPE_SECRET_KEY=your_secret_key_here
emember, never commit your .env.local
file to version control!
3. In stripe account create Api key
In your Stripe account, click on ‘Developer’ and set it to test mode. Then, search for ‘API’ and create an API key. You will receive two keys: a public key and a secret key. Paste these keys in step 2
4. Setting Up Stripe on the Server Side
Create a new file lib/stripe.ts
to initialize Stripe. The stripe.ts file is crucial because it sets up the Stripe client with the secret key from the environment variables. This is basic code that you need in stripe to carried out payment
import Stripe from "stripe";
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is not set");
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
});
export default stripe;
export { Stripe };
export const createPaymentIntent = async (
amount: number,
metadata: any
): Promise<Stripe.PaymentIntent> => {
return stripe.paymentIntents.create({
amount,
currency: "eur", // Changed to EUR
metadata,
});
};
export const constructWebhookEvent = (
payload: string | Buffer,
sig: string
): Stripe.Event => {
return stripe.webhooks.constructEvent(
payload,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
};
The Stripe client is used for various operations, including:
- Creating and managing payment intents.
- Handling webhook events.
- Creating, updating, retrieving, and deleting payment methods.
- Listing all payment methods.
- Attaching, detaching, and confirming payment methods.
- Canceling, capturing, and refunding payments.
Here are optional code that you can add for additional function if you need.
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('Missing STRIPE_SECRET_KEY environment variable');
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
appInfo: {
name: 'Your App Name',
version: '0.1.0',
},
});
export default stripe;
// Utility functions for common Stripe operations
export const createPaymentIntent = async (amount: number, currency: string = 'usd') => {
return stripe.paymentIntents.create({
amount,
currency,
});
};
export const retrievePaymentIntent = async (paymentIntentId: string) => {
return stripe.paymentIntents.retrieve(paymentIntentId);
};
export const cancelPaymentIntent = async (paymentIntentId: string) => {
return stripe.paymentIntents.cancel(paymentIntentId);
};
export const listPaymentMethods = async (customerId: string, type: string = 'card') => {
return stripe.paymentMethods.list({
customer: customerId,
type,
});
};
export const attachPaymentMethod = async (paymentMethodId: string, customerId: string) => {
return stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
};
export const detachPaymentMethod = async (paymentMethodId: string) => {
return stripe.paymentMethods.detach(paymentMethodId);
};
export const createRefund = async (paymentIntentId: string, amount?: number) => {
return stripe.refunds.create({
payment_intent: paymentIntentId,
amount,
});
};
export const constructWebhookEvent = (payload: string | Buffer, signature: string, webhookSecret: string) => {
return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
};
5. Creating a Payment Intent API at backend
Next, let’s create an API route to handle creating a Payment Intent. Create a file app/api/
payment-intents/[id]/route.ts file. This route retrieves the client secret for a Stripe PaymentIntent. The client secret is used to confirm the payment intent on the client-side and is only accessible to the user who created the payment intent.
import { NextRequest, NextResponse } from "next/server";
import { createNextResponse } from "@/lib/ApiResponse";
import stripe from "@/lib/stripe";
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const paymentIntentId = params.id;
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
if (!paymentIntent) {
return createNextResponse(false, "PaymentIntent not found", 404);
}
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
});
} catch (error) {
console.error("Error retrieving PaymentIntent:", error);
if (error instanceof stripe.errors.StripeError) {
if (error.type === "StripeInvalidRequestError") {
return createNextResponse(false, "Invalid PaymentIntent ID", 400);
}
}
// Fallback for other errors
return createNextResponse(false, "Failed to retrieve PaymentIntent", 500);
}
}
6. Implementing Stripe Elements on the Client Side
Now, let’s create a component to handle payments. Create a new file components/CheckoutForm.tsx
. I have different client implementation because of complex UI which you might have different.
import React, { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const CheckoutForm = () => {
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
setProcessing(true);
if (!stripe || !elements) {
return;
}
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement),
});
if (error) {
setError(error.message);
setProcessing(false);
} else {
// Send paymentMethod.id to your server
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 }) // $10.00
});
const data = await response.json();
const result = await stripe.confirmCardPayment(data.clientSecret, {
payment_method: paymentMethod.id
});
if (result.error) {
setError(result.error.message);
} else {
// Payment succeeded
console.log('Payment successful!');
}
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
{error && <div>{error}</div>}
<button type="submit" disabled={!stripe || processing}>
Pay
</button>
</form>
);
};
export default CheckoutForm;
But i have done it different, as the lots of other components, i have created checkout page where i called api/create-payment-intent which inside the services/orderService.ts file which you dont have to follow if you want simple stripe implementation.
import axios from "axios";
import { IOrder, IOrderProduct } from "@/types";
const API_URL = "/api/orders";
interface OrderResponse {
success: boolean;
message: string;
order?: IOrder;
clientSecret?: string;
}
interface OrdersResponse {
success: boolean;
message: string;
orders?: IOrder[];
totalOrders?: number;
currentPage?: number;
totalPages?: number;
}
export interface PaymentIntentResponse {
clientSecret: string;
}
export const orderService = {
createOrder: async (orderData: {
products: IOrderProduct[];
totalItems: number;
totalPrice: number;
addressId: string;
paymentMethod: string;
}): Promise<OrderResponse> => {
try {
const response = await axios.post<OrderResponse>(API_URL, orderData);
return response.data;
} catch (error) {
console.error("Error creating order:", error);
throw error;
}
},
fetchOrders: async (
page: number = 1,
limit: number = 10
): Promise<OrdersResponse> => {
try {
const response = await axios.get<OrdersResponse>(
`${API_URL}?page=${page}&limit=${limit}`
);
return response.data;
} catch (error) {
console.error("Error fetching orders:", error);
throw error;
}
},
getOrderDetails: async (orderId: string): Promise<OrderResponse> => {
try {
const response = await axios.get<OrderResponse>(`${API_URL}/${orderId}`);
return response.data;
} catch (error) {
console.error(
`Error fetching order details for order ${orderId}:`,
error
);
throw error;
}
},
updateOrderStatus: async (
orderId: string,
newStatus: string
): Promise<OrderResponse> => {
try {
const response = await axios.patch<OrderResponse>(
`${API_URL}/${orderId}`,
{
status: newStatus,
}
);
return response.data;
} catch (error) {
console.error(`Error updating order status for order ${orderId}:`, error);
throw error;
}
},
fetchPaymentIntent: async (
paymentIntentId: string
): Promise<PaymentIntentResponse> => {
try {
const response = await axios.get<PaymentIntentResponse>(
`/api/payment-intents/${paymentIntentId}`
);
return response.data;
} catch (error) {
console.error("Error fetching payment intent:", error);
throw error;
}
},
};
Code with highlight red is api call for payment-intents and inside the checkout i have called when user click on order button.
const handlePlaceOrder = async () => {
if (!selectedAddress) {
errorToast(
"Address Required",
"Please provide a delivery address before placing the order."
);
return;
}
setIsLoading(true);
try {
const orderData = {
products: cartItems,
totalItems: cartItems.reduce((sum, item) => sum + item.quantity, 0),
totalPrice,
addressId: selectedAddress._id,
paymentMethod,
};
const response = await orderService.createOrder(orderData);
if (response.success) {
if (paymentMethod === "stripe") {
if (!response.clientSecret && response.order?.paymentIntentId) {
const paymentIntentData = await orderService.fetchPaymentIntent(
response.order.paymentIntentId
);
setClientSecret(paymentIntentData.clientSecret);
setClientSecret(paymentIntentData.clientSecret);
} else {
setClientSecret(response.clientSecret || null);
}
setStep(CheckoutStep.Payment);
} else {
handlePaymentSuccess();
}
} else {
throw new Error(response.message || "Failed to create order");
}
} catch (error) {
console.error("Order creation error:", error);
errorToast(
"Order Failed",
error instanceof Error ? error.message : "An error occurred"
);
} finally {
setIsLoading(false);
}
};
Wrapping It All Together
At client let’s use our CheckoutForm
in a page. Update your page.tsx file
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import CheckoutForm from '../components/CheckoutForm';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
export default function Home() {
return (
<div>
<h1>Stripe Payment Demo</h1>
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
</div>
);
}
Implementing Stripe Webhooks
The webhook is triggered when a payment fails due to insufficient funds, expired card, etc. You can use this webhook to cancel the order, send an email to the customer with instructions on how to resolve the issue, etc. In api/webhooks/route.ts file
import { NextRequest, NextResponse } from "next/server";
import { constructWebhookEvent } from "@/lib/stripe";
import { createNextResponse } from "@/lib/ApiResponse";
import OrderModel from "@/model/Order";
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get("stripe-signature");
if (!sig) {
return createNextResponse(false, "No Stripe signature found", 400);
}
try {
const event = constructWebhookEvent(body, sig);
switch (event.type) {
case "payment_intent.succeeded":
return await handlePaymentIntentSucceeded(event);
case "payment_intent.payment_failed":
return await handlePaymentIntentFailed(event);
// Add more event types as needed
default:
console.log(`Unhandled event type ${event.type}`);
return createNextResponse(true, "Webhook received", 200);
}
} catch (err) {
console.error(
`Webhook Error: ${err instanceof Error ? err.message : "Unknown error"}`
);
return createNextResponse(
false,
`Webhook Error: ${err instanceof Error ? err.message : "Unknown error"}`,
400
);
}
}
async function handlePaymentIntentSucceeded(event: any) {
const paymentIntent = event.data.object;
const order = await OrderModel.findOneAndUpdate(
{ paymentIntentId: paymentIntent.id },
{ status: "paid", paymentStatus: "paid" },
{ new: true }
);
if (!order) {
console.error(`Order not found for PaymentIntent: ${paymentIntent.id}`);
return createNextResponse(false, "Order not found", 404);
}
// Add logic for sending confirmation emails, updating inventory, etc.
return createNextResponse(true, "Payment processed successfully", 200);
}
async function handlePaymentIntentFailed(event: any) {
const paymentIntent = event.data.object;
const order = await OrderModel.findOneAndUpdate(
{ paymentIntentId: paymentIntent.id },
{ status: "payment_failed", paymentStatus: "failed" },
{ new: true }
);
if (!order) {
console.error(`Order not found for PaymentIntent: ${paymentIntent.id}`);
return createNextResponse(false, "Order not found", 404);
}
// Add logic for handling failed payments (e.g., notifying the user)
return createNextResponse(true, "Payment failure handled", 200);
}
export const config = {
api: {
bodyParser: false,
},
};
Best Practices for Integrating Stripe in Next.js
- Use API Routes for Secure Operations: Always use Next.js API routes for operations that require the secret key, such as creating PaymentIntents.
- Separate Concerns: Keep Stripe-related logic in separate files or services for better organization.
- Error Handling: Implement robust error handling for Stripe operations and provide meaningful error messages to users.
- Webhook Handling: Set up a webhook endpoint to handle asynchronous events from Stripe.
- Environment-based Configuration: Use different Stripe accounts and API keys for development and production environments.
Conclusion:
Integrating Stripe into a Next.js application opens up a world of possibilities for handling online payments. By following this guide, you’ve set up a solid foundation for processing payments securely and efficiently. Remember, this is just the beginning – Stripe offers many more features like subscriptions, invoicing, and connect platforms that you can explore to enhance your payment system further.
As you continue to develop your application, always prioritize security, test thoroughly, and stay updated with the latest best practices from both Next.js and Stripe. Happy
Related blog post on nextjs
- Mastering generateStaticParams() in Next.js 14: Boost Performance and SEO
- Understanding Nextjs Server Actions and Mutations 2024
- Understanding Routing in Next.js
- Understanding Prefetching in Next.js
- Data Fetching and Caching in Next.js