Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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>
);
}// 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>
);
}stripe npm package is the server-side Node.js SDK. It should only be imported in server code (Server Actions, Route Handlers, server components).@stripe/stripe-js provides the loadStripe function 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-js provides React components (Elements, PaymentElement, CardElement) and hooks (useStripe, useElements) that wrap Stripe.js.Elements provider must wrap any component that uses Stripe hooks or elements. It initializes Stripe.js and makes it available via React Context.loadStripe returns a Promise that resolves once the script loads. Passing this Promise directly to Elements is the recommended pattern -- it handles the async loading internally.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>Stripe constructor from the stripe package is typed with the API version. Always pass an explicit apiVersion to avoid type mismatches.loadStripe returns Promise<Stripe | null>. The Elements component handles the null case internally.Stripe as a type from stripe for server-side typing, and Stripe from @stripe/stripe-js for 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";stripe (server SDK) in client components. It exposes your secret key and will fail at build time.NEXT_PUBLIC_ to be available in client-side code in Next.js.loadStripe should be called outside of components (at module scope) to avoid recreating the Stripe instance on every render.apiVersion passed to the server SDK should match the version your Stripe Dashboard is configured for. Pinning it avoids surprises when Stripe releases new API versions.| 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.
| 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 |
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)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.
stripe package depends on Node.js APIs not available in the browserCalling 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.
useStripe, useElements, or Stripe Element components must be a descendant of <Elements>loadStripe Promise directly and handles async loading internallyWrap 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.
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.
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"; // client<Elements
stripe={stripePromise}
options={{ clientSecret: "pi_xxx_secret_yyy" }}
>
{children}
</Elements>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.
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.
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.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥