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 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# oryarn create next-app --example with-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.
# Stripe keysNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_12345STRIPE_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:
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 ./pages/api
folder create a new API route: checkout_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) => { e.preventDefault(); // Create a Checkout Session. const checkoutSession: Stripe.Checkout.Session = await fetchPostJSON( '/api/checkout_sessions', { amount: input.customDonation }, );
if ((checkoutSession as any).statusCode === 500) { console.error((checkoutSession as any).message); return; }
// 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. sessionId: checkoutSession.id, }); // If `redirectToCheckout` fails due to a browser or network // error, display the localized error message to your customer // using `error.message`. console.warn(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 /> </div> </Layout> );};
export default DonatePage;
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.tsimport 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}`) return }
// Successfully constructed event console.log('✅ Success:', event.id)// ...
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.
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.