DEV Community

Cover image for Stripe Checkout With Next.js - The Complete Guide
Ashik Chapagain for ByteSlash

Posted on

Stripe Checkout With Next.js - The Complete Guide

Hello world

My name is Ashik Chapagain.

  • A full-stack web developer
  • Content Creator
  • College student
  • Nepalese

I'll walk you through the entire process of using Stripe Checkout with Next.js in this article.

Index

Introduction

Let's see the brief introduction of Stripe and Stripe Checkout.

Stripe: Stripe is an online payment processing and credit card processing platform for businesses.

Stripe Checkout: Stripe Checkout is prebuilt, hosted payment page optimized for conversion.

Demo

Live Preview: https://stripe-checkout-next-js-demo.vercel.app/

Github: https://github.com/projectashik/stripe-checkout-next-js-demo

Let's Build

Installation

As mentioned in the title, we'll be using Next.js for building this.

For Next.js to work, we need to have Node.js and npm installed.

So, first, install Node.js if you haven't yet.
You can follow the following guides to install Node.js.

Optional:
If you want to use yarn, install it using npm.



npm install --global yarn


Enter fullscreen mode Exit fullscreen mode

Now, create a new Next.js app.



npx create-next-app stripe-checkout

yarn create next-app stripe-checkout



Enter fullscreen mode Exit fullscreen mode

I have used tailwindcss for styling the components. So, let's install tailwindcss too.



yarn add --dev tailwindcss@latest postcss@latest autoprefixer@latest

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest


Enter fullscreen mode Exit fullscreen mode

Then run the following command to create tailwindcss config file and postcss config file.



npx tailwindcss init -p


Enter fullscreen mode Exit fullscreen mode

Now open the project in your favorite text editor or IDE (Integrated Development Environment).

For me, it's Visual Studio Code .



# Run the following command to open the project in VS Code.
code .


Enter fullscreen mode Exit fullscreen mode

Now, open tailwind.config.js and update purge option and add mode: 'jit'.



// tailwind.config.js

module.exports = {
  purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  mode: 'jit',
  ...
};


Enter fullscreen mode Exit fullscreen mode

Then, open styles/globals.css and remove all the content and add the following content.



@tailwind base;
@tailwind utilities;
@tailwind components;



Enter fullscreen mode Exit fullscreen mode

*Short Method: *
If you want to use the short process.
Just run the following command.



npx create-next-app -e with-tailwindcss stripe-checkout
yarn create next-app -e with-tailwindcss stripe-checkout


Enter fullscreen mode Exit fullscreen mode

Now, start the dev server and start coding.



yarn dev
npm run dev


Enter fullscreen mode Exit fullscreen mode

Cleaning

Remove the main and footer tags from pages/index.js.

If you want to edit the title and description in the <Head> tag. Then you can.



// pages/index.js
import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Stripe Checkout with Next.js</title>
        <meta name='description' content='Complete Step By Step Tutorial for integrating Stripe Checkout with Next.js' />
        <link rel='icon' href='/favicon.ico' />
      </Head>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

And remove all the styles except for .container in styles/Home.module.css.



/* styles/Home.module.css */
.container {
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
}


Enter fullscreen mode Exit fullscreen mode

Making UI

First, we'll build an index page in which we'll have one item that users can purchase.

Let's add details of an item to an object.



// pages/index.js

const [item, setItem] = useState({
    name: 'Apple AirPods',
    description: 'Latest Apple AirPods.',
    image:
      'https://images.unsplash.com/photo-1572569511254-d8f925fe2cbb?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1400&q=80',
    quantity: 0,
    price: 999,
  });


Enter fullscreen mode Exit fullscreen mode

Don't forget to import the useState hook.



import { useState } from 'react';


Enter fullscreen mode Exit fullscreen mode

Since we're using Unsplash for images, so we need to configure images.unsplash.com under the images section in next.config.js.



module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com'],
  },
};


Enter fullscreen mode Exit fullscreen mode

Now, let's display the following details in the UI and add buttons to increase and decrease the quantity users want to purchase.

In, pages/index.js



...
<main>
  <div className='shadow-lg border rounded p-2 '>
    <Image src={item.image} width={300} height={150} alt={item.name} />
    <h2 className='text-2xl'>$ {item.price}</h2>
    <h3 className='text-xl'>{item.name}</h3>
    <p className='text-gray-500'>{item.description}</p>
    <p className='text-sm text-gray-600 mt-1'>Quantity:</p>
    <div className='border rounded'>
      <button
        className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
      >
        -
      </button>
      <input
        type='number'
        className='p-2'
        defaultValue={item.quantity}
      />
      <button
        className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
      >
        +
      </button>
    </div>
    <p>Total: ${item.quantity * item.price}</p>
    <button
      disabled={item.quantity === 0}
      className='bg-blue-500 hover:bg-blue-600 text-white block w-full py-2 rounded mt-2 disabled:cursor-not-allowed disabled:bg-blue-100'
    >
      Buy
    </button>
  </div>
</main>
...


Enter fullscreen mode Exit fullscreen mode

Now your page should look like this.

UI demo

Now, let's make the button to increase and decrease the quantity working.

Now, we'll create the onQuantityPlus and onQuantityMinus functions to increase and decrease the item quantity on button clicks.



const changeQuantity = (value: number) => {
  // Don't allow the quantity less than 0, if the quantity is greater than value entered by user then the user entered quantity is used, else 0
  setItem({ ...item, quantity: Math.max(0, value) });
};

const onQuantityPlus = () => {
  changeQuantity(item.quantity + 1);
};

const onQuantityMinus = () => {
  changeQuantity(item.quantity - 1);
};


Enter fullscreen mode Exit fullscreen mode

Now, add this function in the onClick event of the respective buttons.



...
<button
  onClick={onQuantityMinus}
  className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
>
  -
</button>
...
<button
  onClick={onQuantityPlus}
  className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
>
  +
</button>
...


Enter fullscreen mode Exit fullscreen mode

Now, let's add the onChange event on the input field to update the value of the item quantity and also don't forget to change defaultValue to value.



...
<input
  type='number'
  className='p-2'
  onChange={onInputChange}
  value={item.quantity}
/>
...


Enter fullscreen mode Exit fullscreen mode

Now, let's create onInputChange function.



...
//
const onInputChange = (e) => {
    changeQuantity(parseInt(e.target.value));
};
...


Enter fullscreen mode Exit fullscreen mode

Demo of working item plus and minus

Creating the Backend

First, we'll start by installing the stripe library.



yarn add stripe
npm install stripe


Enter fullscreen mode Exit fullscreen mode

Now, let's add the required environment variables.
Create a new file .env.local in the root directory with the following data.



NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=YOUR_STRIPE_PUBLISHABLE_KEY
STRIPE_SECRET_KEY=YOUR_STRIPE_SECRET_KEY


Enter fullscreen mode Exit fullscreen mode

You can get these credentials from Dashboard -> Developers -> API Keys.

Getting Stripe Credentials

Now, we need to build an API to get the session id that is required for redirecting the user to the checkout page.

Create a new file in api/create-stripe-session.js. And add the following.



const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

export default async (req, res) => {
  const { item } = req.body;
};


Enter fullscreen mode Exit fullscreen mode

Here, we are getting item details via a post method that we will call on the front end.

Creating the shape for the item needed by Stripe.

There is a particular type of object which Stripe expects to get, this is the object. You should use your local currency instead of "usd" if you want.



const transformedItem = {
    price_data: {
      currency: 'usd',
      product_data: {
        images: [item.image],
        name: item.name,
      },
      unit_amount: item.price * 100,
    },
    description: item.description,
    quantity: item.quantity,
  };


Enter fullscreen mode Exit fullscreen mode

*Creating Stripe Session in the backend: *

You will need to create a stripe session object where you need to define some data.



const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card'],
  line_items: [transformedItem],
  mode: 'payment',
  success_url: redirectURL + '?status=success',
  cancel_url: redirectURL + '?status=cancel',
  metadata: {
    images: item.image,
  },
});

res.json({ id: session.id });


Enter fullscreen mode Exit fullscreen mode
  • payment_method_type: In this, we add the payment methods to pay the price of the product. Click here to know more payment methods.

  • success_url: In success_url, you define where the user will go after the payment is successful.

  • cancel_url: In the cancel_url, you define where the user will go if the user clicks the back button. It can be a cancel page or the checkout page as well.

  • metadata: In metadata, we will add images of the product, if you want you can add other options too.

For other options, click here to know.

Now, the final create-stripe-session.js file should look like this.



const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function CreateStripeSession(req, res) {
  const { item } = req.body;

  const redirectURL =
    process.env.NODE_ENV === 'development'
      ? 'http://localhost:3000'
      : 'https://stripe-checkout-next-js-demo.vercel.app';

  const transformedItem = {
    price_data: {
      currency: 'usd',
      product_data: {
        images: [item.image],
        name: item.name,
      },
      unit_amount: item.price * 100,
    },
    description: item.description,
    quantity: item.quantity,
  };

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [transformedItem],
    mode: 'payment',
    success_url: redirectURL + '?status=success',
    cancel_url: redirectURL + '?status=cancel',
    metadata: {
      images: item.image,
    },
  });

  res.json({ id: session.id });
}

export default CreateStripeSession;


Enter fullscreen mode Exit fullscreen mode

Now, our backend is ready, now we have to send a POST request to API to get the session.

Redirecting to Stripe Checkout Page

For redirecting to Stripe Checkout Page. We have to install the following libraries.



yarn add @stripe/stripe-js axios
npm install @stripe/stripe-js axios


Enter fullscreen mode Exit fullscreen mode

First, we'll first create a stripePromise variable.



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


Enter fullscreen mode Exit fullscreen mode

Now, we'll create createCheckoutSession function to get the Stripe Session for the checkout.



const createCheckOutSession = async () => {
  const stripe = await stripePromise;
  const checkoutSession = await axios.post('/api/create-stripe-session', {
    item: item,
  });
  const result = await stripe.redirectToCheckout({
    sessionId: checkoutSession.data.id,
  });
  if (result.error) {
    alert(result.error.message);
  }
};


Enter fullscreen mode Exit fullscreen mode

Don't forget to import loadStripe and axios.



import { loadStripe } from '@stripe/stripe-js';
import axios from 'axios';


Enter fullscreen mode Exit fullscreen mode

Now, we have to call this function while the user clicks the Buy button.
And onClick={createCheckoutSession}



<button
  disabled={item.quantity === 0}
  onClick={createCheckOutSession}
  className='bg-blue-500 hover:bg-blue-600 text-white block w-full py-2 rounded mt-2 disabled:cursor-not-allowed disabled:bg-blue-100'
>
  Buy
</button>


Enter fullscreen mode Exit fullscreen mode

Now, let's test the checkout.

Since we have updated next.config.js and added the .env.local file, so restart the dev server.



yarn dev
npm run dev


Enter fullscreen mode Exit fullscreen mode

Now, the result should look like this.

Credit Card Payment Demo

Displaying Success and Cancel Message

If you guys remember, then we have used ?status= query parameter in success_url and cancel_url.



// pages/api/create-stripe-session.js
...
const session = await stripe.checkout.sessions.create({
    ...
    success_url: redirectURL + '?status=success',
    cancel_url: redirectURL + '?status=cancel',
    ...
  });

...


Enter fullscreen mode Exit fullscreen mode

So, with this query parameter, we'll know, whether the payment is successful or canceled. And show the respective message.

First, let's fetch the status.



// pages/index.js

import { useRouter } from 'next/router';

...
const router = useRouter();
const { status } = router.query;


Enter fullscreen mode Exit fullscreen mode

Now, add the following below the starting main tag, to display the message.



// pages/index.js

{status && status === 'success' && (
  <div className='bg-green-100 text-green-700 p-2 rounded border mb-2 border-green-700'>
    Payment Successful
  </div>
)}
{status && status === 'cancel' && (
  <div className='bg-red-100 text-red-700 p-2 rounded border mb-2 border-red-700'>
    Payment Unsuccessful
  </div>
)}


Enter fullscreen mode Exit fullscreen mode

Now, the result should be like this.

Alert Message Demo

Our app took some time to assign a stripe session. So, during that time, let's display Processing... instead of Buy inside the button as shown in the demo.

For that,
Create a new state named loading with default value false.



const [loading, setLoading] = useState(false);


Enter fullscreen mode Exit fullscreen mode

Then, update loading value to true, while creating Stripe Session. and again update loading value to false when the stripe session is created.



const createCheckOutSession = async () => {
  setLoading(true);
  ...
  setLoading(false);
};


Enter fullscreen mode Exit fullscreen mode

And now, update the Buy Button.



<button
  disabled={item.quantity === 0 || loading}
  onClick={createCheckOutSession}
  className='bg-blue-500 hover:bg-blue-600 text-white block w-full py-2 rounded mt-2 disabled:cursor-not-allowed disabled:bg-blue-100'
>
  {loading ? 'Processing...' : 'Buy'}
</button>


Enter fullscreen mode Exit fullscreen mode

Here, we're disabling the button while loading and displaying Processing... while loading.

Now, the result should be following:

Button Processing Demo

All done.

Files

At last, your pages/index.js file should look like below:



import Head from 'next/head';
import Image from 'next/image';
import styles from '../styles/Home.module.css';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import axios from 'axios';
import { useRouter } from 'next/router';

export default function Home() {
  const router = useRouter();
  const { status } = router.query;

  const [loading, setLoading] = useState(false);

  const [item, setItem] = useState({
    name: 'Apple AirPods',
    description: 'Latest Apple AirPods.',
    image:
      'https://images.unsplash.com/photo-1572569511254-d8f925fe2cbb?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1400&q=80',
    quantity: 0,
    price: 999,
  });

  const changeQuantity = (value) => {
    // Don't allow the quantity less than 0, if the quantity is greater than value entered by user then the user entered quantity is used, else 0
    setItem({ ...item, quantity: Math.max(0, value) });
  };

  const onInputChange = (e) => {
    changeQuantity(parseInt(e.target.value));
  };

  const onQuantityPlus = () => {
    changeQuantity(item.quantity + 1);
  };

  const onQuantityMinus = () => {
    changeQuantity(item.quantity - 1);
  };

  const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
  const stripePromise = loadStripe(publishableKey);
  const createCheckOutSession = async () => {
    setLoading(true);
    const stripe = await stripePromise;
    const checkoutSession = await axios.post('/api/create-stripe-session', {
      item: item,
    });
    const result = await stripe.redirectToCheckout({
      sessionId: checkoutSession.data.id,
    });
    if (result.error) {
      alert(result.error.message);
    }
    setLoading(false);
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Stripe Checkout with Next.js</title>
        <meta
          name='description'
          content='Complete Step By Step Tutorial for integrating Stripe Checkout with Next.js'
        />
        <link rel='icon' href='/favicon.ico' />
      </Head>
      <main>
        {status && status === 'success' && (
          <div className='bg-green-100 text-green-700 p-2 rounded border mb-2 border-green-700'>
            Payment Successful
          </div>
        )}
        {status && status === 'cancel' && (
          <div className='bg-red-100 text-red-700 p-2 rounded border mb-2 border-red-700'>
            Payment Unsuccessful
          </div>
        )}
        <div className='shadow-lg border rounded p-2 '>
          <Image src={item.image} width={300} height={150} alt={item.name} />
          <h2 className='text-2xl'>$ {item.price}</h2>
          <h3 className='text-xl'>{item.name}</h3>
          <p className='text-gray-500'>{item.description}</p>
          <p className='text-sm text-gray-600 mt-1'>Quantity:</p>
          <div className='border rounded'>
            <button
              onClick={onQuantityMinus}
              className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
            >
              -
            </button>
            <input
              type='number'
              className='p-2'
              onChange={onInputChange}
              value={item.quantity}
            />
            <button
              onClick={onQuantityPlus}
              className='bg-blue-500 py-2 px-4 text-white rounded hover:bg-blue-600'
            >
              +
            </button>
          </div>
          <p>Total: ${item.quantity * item.price}</p>
          <button
            disabled={item.quantity === 0 || loading}
            onClick={createCheckOutSession}
            className='bg-blue-500 hover:bg-blue-600 text-white block w-full py-2 rounded mt-2 disabled:cursor-not-allowed disabled:bg-blue-100'
          >
            {loading ? 'Processing...' : 'Buy'}
          </button>
        </div>
      </main>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

And pages/api/create-stripe-sessoin.js should look like this.



const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function CreateStripeSession(req, res) {
const { item } = req.body;

const redirectURL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: 'https://stripe-checkout-next-js-demo.vercel.app';

const transformedItem = {
price_data: {
currency: 'usd',
product_data: {
images: [item.image],
name: item.name,
},
unit_amount: item.price * 100,
},
description: item.description,
quantity: item.quantity,
};

const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [transformedItem],
mode: 'payment',
success_url: redirectURL + '?status=success',
cancel_url: redirectURL + '?status=cancel',
metadata: {
images: item.image,
},
});

res.json({ id: session.id });
}

export default CreateStripeSession;

Enter fullscreen mode Exit fullscreen mode




Conclusion

So, we have came to end. Hope you like this article. Please consider giving feedback on my writing.

Important Links

Credits

Connect with me:

Top comments (2)

Collapse
 
developerridoy profile image
Ridoy (mern stack)

you didn't add webhook to actually handle the order

Collapse
 
james_takahashi_77908481e profile image
James Takahashi

This article is very helpfull.
Thank you.