Getting Started with Stripe
Recipe
Install the three Stripe packages, configure environment variables, create server and client Stripe instances, and wrap your app with the Elements provider.
npm install stripe @stripe/stripe-js @stripe/react-stripe-jsSet up environment variables in .env.local:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...Create the server-side Stripe instance:
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
typescript: true,
});Create the client-side Stripe loader:
// lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";
export const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);Wrap your app with the Elements provider:
// app/providers.tsx
"use client";
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
export function StripeProvider({ children }: { children: React.ReactNode }) {
return <Elements stripe={stripePromise}>{children}</Elements>;
}// app/layout.tsx
import { StripeProvider } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<StripeProvider>{children}</StripeProvider>
</body>
</html>
);
}Working Example
// app/checkout/page.tsx
"use client";
import { useStripe, useElements } from "@stripe/react-stripe-js";
export default function CheckoutPage() {
const stripe = useStripe();
const elements = useElements();
if (!stripe || !elements) {
return <div>Loading Stripe...</div>;
}
return (
<div>
<h1>Checkout</h1>
<p>Stripe loaded successfully. Ready to accept payments.</p>
<p>
Use test card: <code>4242 4242 4242 4242</code> with any future
expiry and any CVC.
</p>
</div>
);
}Deep Dive
How It Works
- The
stripenpm package is the server-side Node.js SDK. It should only be imported in server code (Server Actions, Route Handlers, server components). @stripe/stripe-jsprovides theloadStripefunction that asynchronously loads the Stripe.js script from Stripe's CDN. This keeps PCI compliance simple because card data never touches your server.@stripe/react-stripe-jsprovides React components (Elements,PaymentElement,CardElement) and hooks (useStripe,useElements) that wrap Stripe.js.- The
Elementsprovider must wrap any component that uses Stripe hooks or elements. It initializes Stripe.js and makes it available via React Context. loadStripereturns a Promise that resolves once the script loads. Passing this Promise directly toElementsis the recommended pattern -- it handles the async loading internally.
Variations
Lazy-load Elements only on checkout pages:
// app/checkout/layout.tsx
"use client";
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
export default function CheckoutLayout({
children,
}: {
children: React.ReactNode;
}) {
return <Elements stripe={stripePromise}>{children}</Elements>;
}Pass a client secret to Elements for PaymentIntent flows:
<Elements
stripe={stripePromise}
options={{ clientSecret: "pi_xxx_secret_yyy" }}
>
{children}
</Elements>TypeScript Notes
- The
Stripeconstructor from thestripepackage is typed with the API version. Always pass an explicitapiVersionto avoid type mismatches. loadStripereturnsPromise<Stripe | null>. TheElementscomponent handles thenullcase internally.- Import
Stripeas a type fromstripefor server-side typing, andStripefrom@stripe/stripe-jsfor client-side typing. They are different types.
// Server-side type
import type Stripe from "stripe";
// Client-side type
import type { Stripe as StripeJS } from "@stripe/stripe-js";Gotchas
- Never import
stripe(server SDK) in client components. It exposes your secret key and will fail at build time. - The publishable key must be prefixed with
NEXT_PUBLIC_to be available in client-side code in Next.js. loadStripeshould be called outside of components (at module scope) to avoid recreating the Stripe instance on every render.- In test mode, all API calls use test data. No real charges are created. Switch to live keys only after thorough testing.
- The
apiVersionpassed to the server SDK should match the version your Stripe Dashboard is configured for. Pinning it avoids surprises when Stripe releases new API versions.
Test Card Numbers
| Card Number | Scenario |
|---|---|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 3220 | 3D Secure authentication required |
| 4000 0000 0000 9995 | Declined (insufficient funds) |
| 4000 0000 0000 0002 | Generic decline |
| 4000 0025 0000 3155 | Requires authentication |
Use any future expiration date and any 3-digit CVC for all test cards.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Global Elements provider | Simple setup, works everywhere | Loads Stripe.js on every page |
| Per-page Elements wrapper | Only loads Stripe on checkout pages | Requires wrapping each payment page |
| Dynamic import of Elements | Smallest initial bundle | More complex setup, slight delay on checkout |
FAQs
What are the three npm packages needed for Stripe in a Next.js app, and what does each one do?
stripe-- server-side Node.js SDK for calling the Stripe API@stripe/stripe-js-- client-side loader (loadStripe) that fetches Stripe.js from the CDN@stripe/react-stripe-js-- React components (Elements,PaymentElement) and hooks (useStripe,useElements)
Why must the publishable key use the NEXT_PUBLIC_ prefix?
Next.js only exposes environment variables to client-side code if they start with NEXT_PUBLIC_. Without the prefix, the variable is undefined in the browser and loadStripe will fail silently.
What happens if you import the server-side stripe package in a client component?
- It exposes your secret key to the browser bundle
- The build will fail because the
stripepackage depends on Node.js APIs not available in the browser - Always keep server SDK imports in Server Actions, Route Handlers, or server components only
Why should loadStripe be called at module scope instead of inside a component?
Calling it inside a component body would re-execute on every render, creating a new Stripe instance each time. Calling at module scope ensures a single shared Promise that resolves once.
What is the purpose of the Elements provider?
- It initializes Stripe.js and makes the Stripe instance available via React Context
- Any component using
useStripe,useElements, or Stripe Element components must be a descendant of<Elements> - It accepts the
loadStripePromise directly and handles async loading internally
How do you lazy-load Stripe only on checkout pages instead of globally?
Wrap only the checkout route's layout.tsx with the <Elements> provider instead of the root layout. This way Stripe.js is not loaded on pages that do not need it.
What does the apiVersion option do when creating the server-side Stripe instance?
It pins the Stripe API version your code expects. Without it, Stripe uses your Dashboard's default version, which can change and cause breaking behavior unexpectedly.
Gotcha: What is the difference between the Stripe type from stripe and the Stripe type from @stripe/stripe-js?
They are completely different types. The server-side Stripe (from stripe) types the Node SDK. The client-side Stripe (from @stripe/stripe-js) types the browser Stripe.js instance. Import them separately using import type.
import type Stripe from "stripe"; // server
import type { Stripe as StripeJS } from "@stripe/stripe-js"; // clientHow do you pass a client secret to Elements for a PaymentIntent flow?
<Elements
stripe={stripePromise}
options={{ clientSecret: "pi_xxx_secret_yyy" }}
>
{children}
</Elements>What does loadStripe return and how does Elements handle the null case?
loadStripe returns Promise<Stripe | null>. It resolves to null if the script fails to load. The <Elements> component handles this internally -- hooks like useStripe will return null until loading succeeds.
TypeScript: How should you type the StripeProvider component's props?
function StripeProvider({ children }: { children: React.ReactNode }) {
return <Elements stripe={stripePromise}>{children}</Elements>;
}children is typed as React.ReactNode. The stripe prop on <Elements> accepts Promise<Stripe | null> | Stripe | null.
Gotcha: Can you use test mode and live mode keys in the same environment?
No. Test keys (sk_test_, pk_test_) and live keys (sk_live_, pk_live_) operate in completely separate environments. Mixing them (e.g., server uses live, client uses test) will cause API errors. Always use matching pairs.