Skip to content

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

Add payments functionality to your Next.js applications with Stripe and deploy to Vercel.

5 min read
Last updated June 15, 2026

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

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. 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:

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

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.

terminal
# Stripe keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345

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.

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:

./utils/get-stripejs.ts
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;

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 app/actions/ directory, create a Server Action called stripe.ts. In this function, create a new CheckoutSession and return the session URL to redirect the user to Stripe.

app/actions/stripe.ts
// Partial of app/actions/stripe.ts
"use server";
import type { Stripe } from "stripe";
import { stripe } from "@/lib/stripe";
import { formatAmountForStripe } from "@/utils/stripe-helpers";
import { CURRENCY } from "@/config";
export async function createCheckoutSession(
data: FormData,
): Promise<{ client_secret: string | null; url: string | null }> {
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create({
mode: "payment",
submit_type: "donate",
line_items: [
{
quantity: 1,
price_data: {
currency: CURRENCY,
product_data: { name: "Custom amount donation" },
unit_amount: formatAmountForStripe(
Number(data.get("customDonation") as string),
CURRENCY,
),
},
},
],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/donate-with-checkout`,
});
return {
client_secret: checkoutSession.client_secret,
url: checkoutSession.url,
};
}

Next, create a CheckoutForm component in app/components/CheckoutForm.tsx that calls the Server Action above and redirects the user to Stripe.

app/components/CheckoutForm.tsx
// Partial of app/components/CheckoutForm.tsx
"use client";
import { createCheckoutSession } from "@/actions/stripe";
const formAction = async (data: FormData): Promise<void> => {
const { url } = await createCheckoutSession(data);
window.location.assign(url as string);
};
return (
<form action={formAction}>
<input type="hidden" name="uiMode" value="hosted" />
{/* donation input */}
<button type="submit">Donate</button>
</form>
);

Use this component in your checkout page at app/donate-with-checkout/page.tsx.

app/donate-with-checkout/page.tsx
import type { Metadata } from "next";
import CheckoutForm from "@/components/CheckoutForm";
export const metadata: Metadata = {
title: "Donate with hosted Checkout | Next.js + TypeScript Example",
};
export default function DonatePage(): JSX.Element {
return (
<div className="page-container">
<h1>Donate with hosted Checkout</h1>
<p>Donate to our project 💖</p>
<CheckoutForm uiMode="hosted" />
</div>
);
}

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.

Create a Route Handler at app/api/webhooks/route.ts to receive Stripe webhook events. 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:

app/api/webhooks/route.ts
// Partial of app/api/webhooks/route.ts
import type { Stripe } from "stripe";
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
await (await req.blob()).text(),
req.headers.get("stripe-signature") as string,
process.env.STRIPE_WEBHOOK_SECRET as string,
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
console.log(`❌ Error message: ${errorMessage}`);
return NextResponse.json(
{ message: `Webhook Error: ${errorMessage}` },
{ status: 400 },
);
}
console.log("✅ Success:", event.id);
// ...

This way your Route Handler only processes requests whose signatures were signed by Stripe.

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: https://nextjs-typescript-react-stripe-js.vercel.app/

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.

Was this helpful?

supported.