How to add Stripe payments to your Next.js app

Eli Sultanov
8 min readMay 7, 2023
Photo by Emil Kalibradov on Unsplash

Oh look, I found my medium password!

Now, for my first article, I would like to discuss the integration of Stripe into your Next.js app.

In this project, our focus will not be on creating something overly complex. Instead, we will be developing a straightforward Next.js 13.4 app that facilitates one-time Stripe payments.

The Setup

Open up your terminal and run the following command inside your designated folder, in my case it’s Workspace.

npx create-next-app@latest

When you run the command, you’ll be asked to install “create-next-app@13.4.1” as well as asked a few questions

  • What is your project named? stripe-nextjs
  • Would you like to use TypeScript with this project? Yes
  • Would you like to use ESLint with this project? Yes
  • Would you like to use Tailwind CSS with this project? No, since we won’t be doing anything with styles
  • Would you like to use `src/` directory with this project? Yes
  • Use App Router (recommended)? Yes
  • Would you like to customize the default import alias? No

Once you’ve answered all of the questions, the installation will begin!

At this point the installation is complete! Now, to see your live Next.js app in action, just run the following command in your terminal. Make sure you’re still inside your Next.js app.

yarn run dev

Before we go any further, let’s create a stripe account and get our api keys.

After you made a stripe account, go to dashboard.stripe.com/test/apikeys and get your api keys.

Setup your env file

Inside your root directory, create a the following file, .env.local.

Inside the .env.local file add the following:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=""
STRIPE_SECRET_KEY=""

There are a couple of important points to remember here. First, make sure to replace the “” with your Stripe keys. Secondly, it’s crucial to understand the difference between the two variables: one includes “NEXT_PUBLIC” while the other does not. To put it simply, it’s ok to expose our STRIPE_PUBLISHABLE_KEY using “NEXT_PUBLIC”, as it will be accessible on the client side. However, our secret key should never be exposed to the client side.

Got it, get it, good! Lets move on!

Install required packages

Inside your Next.js app install the following packages:

@stripe/react-stripe-js @stripe/stripe-js stripe axios

Let’s work on our backend

Inside our app folder, create a new folder called api. This is where all of our backend code will live.

Inside the api folder, create a new folder called create-payment-intent and inside that folder create a file called route.ts.

Folder structure
This is how your folder structure should look like at this point.

The reason behind creating a file called “route” inside the “create-payment-intent” folder is to inform Next.js that we are dealing with an API request. As per the official documentation, a route is the most basic level of routing, and it doesn’t affect layouts or client-side navigations like a page does. It’s important to note that a “route.ts” file cannot exist in the same location as a “page.ts” file. I highly recommend you take a look at the documentation for further details.

Here is the code for our route.ts file:

import { NextResponse, NextRequest } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
typescript: true,
apiVersion: "2022-11-15",
});

export async function POST(req: NextRequest) {
const { data } = await req.json();
const { amount } = data;
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: Number(amount) * 100,
currency: "USD",
});

return new NextResponse(paymentIntent.client_secret, { status: 200 });
} catch (error: any) {
return new NextResponse(error, {
status: 400,
});
}
}

let’s break it down and understand what is happening.

First and foremost, we import our types and stripe:

import { NextResponse, NextRequest } from "next/server";
import Stripe from "stripe";

Afterwards we make a stripe instance:

tconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
typescript: true,
apiVersion: "2022-11-15",
});

Our Stripe instance requires the Stripe secret from our env.local file. Furthermore, we have enabled TypeScript for our Stripe instance and specified the version of the Stripe API for the instance.

Before we proceed, let’s discuss how Next.js 13.4 handles API routes.

As you may have noticed, the function’s title is in all capital letters, such as “POST.” This naming convention is necessary because Next.js needs to determine the corresponding HTTP method to execute. For example, if you intend to make a GET request, the function’s title should be “GET,” and similarly for other actions you wish to perform.

Got it, get it, good! Lets move on!

Within our function, we proceed by destructuring the data from the request. Subsequently, we further destructure the amount from the data object.

const { data } = await req.json();
const { amount } = data;

Now, let’s look into our trycatch block where all of our exciting logic happens!

Lets dig in.

const paymentIntent = await stripe.paymentIntents.create({
amount: Number(price) * 100,
currency: "USD",
});

return new NextResponse(paymentIntent.client_secret, { status: 200 });

Within our try block, we will create a payment intent. In case you're not familiar with payment intents, they hold important information about the transaction, including the supported payment methods, the amount to be collected, and the desired currency.

Oh yea almost forgot, make sure to multiply your amount by 100, stripe only works with cents!!!

Finally, we will return a response containing the client secret obtained from Stripe. This step is crucial as we will utilize the client secret on the client side to assist Stripe in confirming the payment.

    return new NextResponse(error, {
status: 400,
});

Inside our catch block, we simply return a response containing the error.

At this point our backend is complete. Let’s move on to our client side where we will interact with our newly created endpoint.

The frontend

Let’s do some clean up before we start doing some magic!

In your layout.tsx file, delete the line that imports your global.css, we won’t be dealing with styles.

In your app.tsx, delete everything that is inside the <main> tags, also delete the Image component import.

Great, at this point your layout.tsx file should look like this:

import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

And your app.tsx should look like this:

export default function Home() {
return (
<></>
);
}

We are on a roll here, let’s start setting up our payment form!

In the “src” folder, go ahead and make a new folder called “components.” Inside that “components” folder, create another folder called “PaymentForm.” In the “PaymentForm” folder, create a file called “PaymentForm.tsx.” That’s where our payment form will live and most of the logic will happen here.

Let’s begin!

Before we start writing our component, we need to let Next.js know that we are dealing with a client component, not a server one.

At the top of the PaymentForm.tsx file, add the following:

"use client";

Afterwords, add the following imports:

import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
import axios from "axios";
import React from "react";

Now let’s create an empty component:

export default function PaymentForm() {
return <></>;
}

By now our file should look like this:

"use client";

import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
import axios from "axios";
import React from "react";

export default function PaymentForm() {
return <></>;
}

The first thing we should do inside our component is to use stripe hooks, the hooks we are going to be using are useStripe and useElements.

  const stripe = useStripe();
const elements = useElements();

Let’s create a function called onSubmit so that we can submit our from.

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {}

Inside our onSubmit let’s prevent default behavior with e.preventDefault();

The next thing we need to do is access the stripe card element with the following snippet:

    const cardElement = elements?.getElement("card");

Time for the actual magic!

Let’s create a try-catch block. Within the try block, we’ll create a conditional statement to check for the presence of both stripe and cardElement. In case either of them is missing, we’ll return null to effectively interrupt the onSubmit function and prevent any further action.

For the catch block simply console.log the error.

Here is the snippet:

    try {
if (!stripe || !cardElement) return null;
} catch (error) {
console.log(error);
}

Ok folks, we are in a good position right now, let’s move on.

Once we have confirmed the presence of both Stripe and cardElement, we can proceed to create a payment intent using the API we previously established in our backend. The data we will pass to the API includes the amount, which, in my case, is set at 89.

    const { data } = await axios.post("/api/create-payment-intent", {
data: { amount: 89 },
});

let’s assign our response to a variable called clientSecret

      const clientSecret = data;

The last thing we need to do is confirm the payment using confirmCardPayment method that is provided by stripe. Here is a code snippet below:

      await stripe?.confirmCardPayment(clientSecret, {
payment_method: { card: cardElement },
});

At this point our onSubmit function is complete.

Let’s make sure our component returns a form that uses the CardElement component provided by stripe.

    <form onSubmit={onSubmit}>
<CardElement />
<button type="submit">Submit</button>
</form>

The final result:

"use client";

import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
import axios from "axios";
import React from "react";

export default function PaymentForm() {
const stripe = useStripe();
const elements = useElements();

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const cardElement = elements?.getElement("card");

try {
if (!stripe || !cardElement) return null;
const { data } = await axios.post("/api/create-payment-intent", {
data: { amount: 89 },
});
const clientSecret = data;

await stripe?.confirmCardPayment(clientSecret, {
payment_method: { card: cardElement },
});
} catch (error) {
console.log(error);
}
};

return (
<form onSubmit={onSubmit}>
<CardElement />
<button type="submit">Submit</button>
</form>
);
}

Great news! Our PaymentForm component is now complete. Hooray! Now, let’s move on to the final step, which involves updating our app.tsx file to utilize our newly created PaymentForm component.

Before our PaymentForm component can be used inside the app.tsx let’s import a few things and load stripe.

"use client";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import PaymentForm from "@/components/PaymentForm/PaymentForm";

Let’s load stripe:

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Make sure you load stripe outside the component, otherwise stripe will render every time and you definitely do not want that.

Lastly, within our return statement, let’s make use of the Element provider provided to us by Stripe. We will wrap our PaymentForm component with it. Here’s how it should look:

    <Elements stripe={stripePromise}>
<PaymentForm />
</Elements>

Here is the final result:

"use client";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import PaymentForm from "@/components/PaymentForm/PaymentForm";
// Make sure to call `loadStripe` outside of a component’s render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

export default function Home() {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
);
}

Look at that! You have created a payment form using stripe, good job!

Hope y’all found this article helpful, I’ll definitely be more active on here going forward, I hope 🤔

Here is the link to the git repo, enjoy!

--

--

Eli Sultanov

Sharing with the world things I discover while coding. Find me on Twitter @elidotsv or at https://elisv.com