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:
1npx create-next-app --example with-typescript my-stripe-project && cd my-stripe-project2# or3yarn 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.
1# Stripe keys2NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_123453STRIPE_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.
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:
1import { loadStripe } from '@stripe/stripe-js';2
3const 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:
1// ./utils/get-stripejs.ts2import { Stripe, loadStripe } from '@stripe/stripe-js';3
4let stripePromise: Promise<Stripe | null>;5const getStripe = () => {6 if (!stripePromise) {7 stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);8 }9 return stripePromise;10};11
12export 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 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.
1// Partial of ./pages/api/checkout_sessions/index.ts2// ...3// Create Checkout Sessions from body params.4const params: Stripe.Checkout.SessionCreateParams = {5 submit_type: 'donate',6 payment_method_types: ['card'],7 line_items: [8 {9 name: 'Custom amount donation',10 amount: formatAmountForStripe(amount, CURRENCY),11 currency: CURRENCY,12 quantity: 1,13 },14 ],15 success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,16 cancel_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,17};18const checkoutSession: Stripe.Checkout.Session =19 await stripe.checkout.sessions.create(params);20// ...
Next, create a CheckoutForm component that calls the above API route to create a CheckoutSession and facilitates the redirect to Stripe.
1// Partial of ./components/CheckoutForm.tsx2// ...3const handleSubmit = async (e: FormEvent) => {4 e.preventDefault();5 // Create a Checkout Session.6 const checkoutSession: Stripe.Checkout.Session = await fetchPostJSON(7 '/api/checkout_sessions',8 { amount: input.customDonation },9 );10
11 if ((checkoutSession as any).statusCode === 500) {12 console.error((checkoutSession as any).message);13 return;14 }15
16 // Redirect to Checkout.17 const stripe = await getStripe();18 const { error } = await stripe!.redirectToCheckout({19 // Make the id field from the Checkout Session creation API response20 // available to this file, so you can provide it as parameter here21 // instead of the {{CHECKOUT_SESSION_ID}} placeholder.22 sessionId: checkoutSession.id,23 });24 // If `redirectToCheckout` fails due to a browser or network25 // error, display the localized error message to your customer26 // using `error.message`.27 console.warn(error.message);28};29// ...
Use this component in your checkout page within the ./pages
directory.
1import { NextPage } from 'next';2import Layout from '../components/Layout';3
4import CheckoutForm from '../components/CheckoutForm';5
6const DonatePage: NextPage = () => {7 return (8 <Layout title="Donate with Checkout | Next.js + TypeScript Example">9 <div className="page-container">10 <h1>Donate with Checkout</h1>11 <p>Donate to our project 💖</p>12 <CheckoutForm />13 </div>14 </Layout>15 );16};17
18export 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
:
1// Partial of ./pages/api/webhooks/index.ts2import Cors from 'micro-cors';3
4const cors = Cors({5 allowMethods: ['POST', 'HEAD'],6});7// ...8export 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:
1// Partial of ./pages/api/webhooks/index.ts2// ...3const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!4
5// Stripe requires the raw body to construct the event.6export const config = {7 api: {8 bodyParser: false,9 },10}11
12const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {13 if (req.method === 'POST') {14 const buf = await buffer(req)15 const sig = req.headers['stripe-signature']!16
17 let event: Stripe.Event18
19 try {20 event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret)21 } catch (err) {22 // On error, log and return the error message23 console.log(`❌ Error message: ${err.message}`)24 res.status(400).send(`Webhook Error: ${err.message}`)25 return26 }27
28 // Successfully constructed event29 console.log('✅ Success:', event.id)30// ...
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: 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.