Getting started with Next.js, TypeScript, and Stripe Checkout

This guide walks you through setting up a Next.js project with TypeScript and adding payments functionality with Stripe Checkout.

Step 1: Setting Up a TypeScript Project with Next.js

Setting up a TypeScript project with Next.js is very convenient, as it automatically generates the tsconfig.json configuration file for you. You can follow the setup steps in the docs or start off with a more complete example. You can also find the full example that we're looking at in detail below, on GitHub.

To create a pre-configured Next.js TypeScript project locally, execute create-next-app with npm or Yarn:

npx create-next-app --example with-typescript my-stripe-project && cd my-stripe-project
# or
yarn create next-app --example with-typescript my-stripe-project && cd my-stripe-project

Managing API Keys with Next.js & Vercel

When working with API keys and secrets, you need to make sure to keep them out of version control. That's why you should set these as environment variables. Find more details on how to organise your .env files in the Next.js docs.

At the root of your project add a .env.local file and provide the Stripe API keys from your Stripe Dashboard.

# Stripe keys

The NEXT_PUBLIC_ prefix automatically exposes this variable to the browser. Next.js will insert the value for these into the publicly viewable source code at build/render time. Therefore make sure to not use this prefix for secret values!

Make sure to add .env*.local to your .gitignore file to tell git to not track your secrets. If you created the project with create-next-app, the .gitignore file is already set up for you.

Loading Stripe.js

Due to PCI compliance requirements, the Stripe.js library has to be loaded from Stripe's servers. This creates a challenge when working with server-side rendered apps, as the window object is not available on the server. To help you manage this, Stripe provides a loading wrapper that allows you to import Stripe.js as an ES module:

import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

Stripe.js is loaded as a side effect of the import '@stripe/stripe-js'; statement. If you prefer to delay loading of Stripe.js until Checkout, you can import {loadStripe} from '@stripe/stripe-js/pure';. Find more details on the various options in the Stripe docs.

To optimize your site's performance you can hold off instantiating Stripe until the first render of your checkout page. To make sure that you don't reinstate Stripe on every render, we recommend that you use the singleton pattern to create/retrieve the Stripe instance:

import { Stripe, loadStripe } from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>;
const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
return stripePromise;
export default getStripe;

Step 2: Creating a CheckoutSession and Redirecting to Stripe Checkout

Stripe Checkout is the fastest way to get started with Stripe and provides a stripe-hosted checkout page that comes with various payment methods and support for Apple Pay and Google Pay out of the box.

In your ./pages/api folder create a new API routecheckout_sessions/index.ts. In this function create a new CheckoutSession and return the its id which is used to initiate the redirect to Stripe.

// Partial of ./pages/api/checkout_sessions/index.ts
// ...
// Create Checkout Sessions from body params.
const params: Stripe.Checkout.SessionCreateParams = {
submit_type: 'donate',
payment_method_types: ['card'],
line_items: [
name: 'Custom amount donation',
amount: formatAmountForStripe(amount, CURRENCY),
currency: CURRENCY,
quantity: 1,
success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create(params);
// ...

Next, create a CheckoutForm component that calls the above API route to create a CheckoutSession and facilitates the redirect to Stripe.

// Partial of ./components/CheckoutForm.tsx
// ...
const handleSubmit = async (e: FormEvent) => {
// Create a Checkout Session.
const checkoutSession: Stripe.Checkout.Session = await fetchPostJSON(
{ amount: input.customDonation },
if ((checkoutSession as any).statusCode === 500) {
console.error((checkoutSession as any).message);
// Redirect to Checkout.
const stripe = await getStripe();
const { error } = await stripe!.redirectToCheckout({
// Make the id field from the Checkout Session creation API response
// available to this file, so you can provide it as parameter here
// instead of the {{CHECKOUT_SESSION_ID}} placeholder.
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `error.message`.
// ...

Use this component in your checkout page within the ./pages directory.

import { NextPage } from 'next';
import Layout from '../components/Layout';
import CheckoutForm from '../components/CheckoutForm';
const DonatePage: NextPage = () => {
return (
<Layout title="Donate with Checkout | Next.js + TypeScript Example">
<div className="page-container">
<h1>Donate with Checkout</h1>
<p>Donate to our project 💖</p>
<CheckoutForm />
export default DonatePage;

Step 3: Handling Webhooks & Checking Their Signatures

Webhook events allow you to get notified about events that happen on your Stripe account. This is especially useful for asynchronous payments, subscriptions with Stripe Billing, or building a marketplace with Stripe Connect.

By default, Next.js API routes are same-origin only. To allow Stripe webhook event requests to reach your API route, add micro-cors:

// Partial of ./pages/api/webhooks/index.ts
import Cors from 'micro-cors';
const cors = Cors({
allowMethods: ['POST', 'HEAD'],
// ...
export default cors(webhookHandler as any);

This, however, means that now anyone can post requests to your API route. To make sure that a webhook event was sent by Stripe, not by a malicious third party, you need to verify the webhook event signature:

// Partial of ./pages/api/webhooks/index.ts
// ...
const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!
// Stripe requires the raw body to construct the event.
export const config = {
api: {
bodyParser: false,
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const buf = await buffer(req)
const sig = req.headers['stripe-signature']!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret)
} catch (err) {
// On error, log and return the error message
console.log(`❌ Error message: ${err.message}`)
res.status(400).send(`Webhook Error: ${err.message}`)
// Successfully constructed event
console.log('✅ Success:',
// ...

This way your API route is able to receive POST requests from Stripe but also makes sure, only requests sent by Stripe are being processed.

Step 4: Deploy with Vercel

To deploy your Next.js + Stripe Checkout site with Vercel for Git, make sure it has been pushed to a Git repository.

Import the project into Vercel using your Git provider of choice.

After your project has been imported, all subsequent pushes to branches will generate Preview Deployments, and all changes made to the Production Branch (commonly "main") will result in a Production Deployment.

Once deployed, you will get a URL to see your site live, such as the following:

Set up a Next.js + Stripe Checkout site with a few clicks using the Deploy button, and create a Git repository for it in the process for automatic deployments for your updates.


Couldn't find the guide you need?