2025-07-05 Web Development, Ecommerce
Stripe + Next.js 15 Test App: Step-by-Step Walkthrough
By O. Wolfson
This guide will walk you through creating a complete test payment app with Stripe and Next.js 15 (App Router), including solutions to common integration questions and gotchas. You’ll build a secure test workflow, see key Stripe/React concepts in action, and understand why each step is important for a smooth dev experience.
Table of Contents
- Overview
- Prerequisites
- Get Stripe Test Keys
- Set Up Next.js Project
- Environment Variables
- Install Stripe Packages
- Create the Stripe API Route
- Create the Stripe Elements Provider
- Build the Checkout Form
- Connect Components
- Test with Stripe Test Cards
- Troubleshooting & Gotchas
- Extras: Customization & Advanced Tips
- Appendix: Example Project Structure
- Summary
1. Overview
This walkthrough shows how to build a working Stripe payment demo in Next.js 15 (with the App Router), using secure best practices, and answers common beginner questions along the way.
2. Prerequisites
- Node.js 18+
- Stripe account (free)
- Familiarity with React basics
3. Get Stripe Test Keys
-
Log in to your Stripe Dashboard.
-
Enable View test data at the top/side toggle.
-
Go to Developers → API keys.
-
Copy your keys:
- Secret key:
sk_test_...
(for backend only) - Publishable key:
pk_test_...
(for frontend)
- Secret key:
Note: Never share your secret key. Don’t mix up sk_
and pk_
!
4. Set Up Next.js Project
bashnpx create-next-app@latest stripe-next-test
cd stripe-next-test
If asked, use the App Router option.
5. Environment Variables
Create a .env.local
file at the project root:
envSTRIPE_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXX NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXX
- Don’t commit this file!
- Publishable key (with
NEXT_PUBLIC_
) is safe for frontend. Secret key is only for server.
6. Install Stripe Packages
bashnpm install stripe @stripe/stripe-js @stripe/react-stripe-js
7. Create the Stripe API Route
File: app/api/create-payment-intent/route.js
or .ts
jsimport { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
try {
const body = await request.json();
const { amount } = body;
const paymentIntent = await stripe.paymentIntents.create({
amount, // amount in cents
currency: "usd",
});
return NextResponse.json({ clientSecret: paymentIntent.client_secret });
} catch (err: unknown) {
if (err instanceof Error) {
return NextResponse.json({ error: err.message }, { status: 400 });
}
return NextResponse.json(
{ error: "An unknown error occurred" },
{ status: 400 }
);
}
}
8. Create the Stripe Elements Provider
File: src/components/stripe-elements-provider.tsx
tsx"use client";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
export default function StripeElementsProvider({
children,
}: {
children: React.ReactNode;
}) {
return <Elements stripe={stripePromise}>{children}</Elements>;
}
- This provider must be implemented as a
"use client"
component. However, in Next.js App Router, you may use it from a Server Component (such as a page or route). Next.js will ensure that the Elements provider and its children run on the client as needed. - Best practice: Do NOT use this provider globally in
layout.tsx
. Instead, use it in your payment page (e.g.,app/checkout/page.tsx
), wrapping only the components that need Stripe context.
9. Build the Checkout Form
File: src/components/checkout-form.tsx
tsx"use client";
import { useState } from "react";
import { useStripe, useElements, CardElement } from "@stripe/react-stripe-js";
export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const res = await fetch("/api/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 1000 }), // $10
});
const { clientSecret, error } = await res.json();
if (error) {
setMessage(error);
setLoading(false);
return;
}
if (!stripe || !elements) {
setMessage("Stripe.js has not loaded yet.");
setLoading(false);
return;
}
const result = await stripe.confirmCardPayment(clientSecret, {
payment_method: { card: elements.getElement(CardElement)! },
});
if (result.error) {
setMessage(result.error.message ?? "An error occurred");
} else if (result.paymentIntent.status === "succeeded") {
setMessage("Payment successful!");
} else {
setMessage("Something went wrong.");
}
setLoading(false);
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe || loading}>
{loading ? "Processing..." : "Pay $10"}
</button>
{message && <div>{message}</div>}
</form>
);
}
- Why is the button disabled? Until Stripe.js loads, the button stays disabled for security.
- Why only one input box?
<CardElement />
combines card number, expiry, and CVC into a single smart input field. Enter all details in sequence! - Common error: “Your card’s expiration date is incomplete.” This just means you need to finish all fields inside
<CardElement />
.
10. Connect Components
File: app/checkout/page.tsx
tsximport StripeElementsProvider from "@/components/stripe-elements-provider";
import CheckoutForm from "@/components/checkout-form";
export default function CheckoutPage() {
return (
<StripeElementsProvider>
<CheckoutForm />
</StripeElementsProvider>
);
}
Do NOT wrap your whole app in <Elements>
in layout.tsx
—that’s a Server Component! Always wrap payment pages in a client provider.
11. Test with Stripe Test Cards
- Card Number:
4242 4242 4242 4242
- Expiry: Any future date (e.g.,
12/34
) - CVC: Any 3 digits (e.g.,
123
) - ZIP: Any 5 digits (e.g.,
12345
)
Input all details in the single CardElement input. Press Tab to move between sub-fields. Use Stripe’s test cards for more scenarios.
12. Troubleshooting & Gotchas
- Button stays disabled: You likely swapped secret/publishable keys or Stripe.js hasn't loaded. Double check your
.env.local
. - Don’t see card input? Make sure
<CardElement />
is in a Client Component, inside<Elements>
. - Got a “createContext only works in Client Components” error? Don’t put Stripe context providers in
layout.tsx
—only in Client Components! - “Expiration date is incomplete” error? Just finish filling out the CardElement’s fields.
- Changes to .env.local not working? Always restart the dev server after editing env files.
13. Extras: Customization & Advanced Tips
- To split card fields, use
<CardNumberElement />
,<CardExpiryElement />
, etc., from Stripe React SDK. - To style CardElement, pass an
options
prop with a style object. - Use the Stripe CLI to test webhooks locally for real-world payment flows.
Next steps:
- Add webhooks to handle post-payment actions
- Add error boundary and advanced error UX
- Deploy to Vercel (update Stripe keys to live mode for production)
14. Appendix: Example Project Structure
Here’s how your Stripe + Next.js 15 project structure might look. This structure keeps code organized and makes it easy to follow the guide:
textstripe-next-test/ ├── app/ │ ├── api/ │ │ └── create-payment-intent/ │ │ └── route.js # API route for PaymentIntent (step 7) │ ├── checkout/ │ │ └── page.tsx # Payment page (step 10) │ └── layout.tsx # App layout (should NOT include Stripe provider) │ └── page.tsx # Home/landing page (can link to /checkout) ├── src/ │ └── components/ │ ├── checkout-form.tsx # Checkout form UI (step 9) │ └── stripe-elements-provider.tsx # Elements provider (step 8) ├── .env.local # Secret & publishable keys (step 5) ├── package.json └── ...
- Adjust
src/
andapp/
split as you prefer for your own project (Next.js supports both top-levelapp/
and colocatedsrc/app/
). - All payment logic, API, and Stripe context is encapsulated in specific files. No secret key or Stripe context in global layout.
Summary
You now have a full Stripe test payment flow in Next.js 15 App Router, with:
- Secure API route for backend logic
- Secure key management
- Card form with
<CardElement />
- Best practices and solutions for common Stripe/Next.js integration questions
Here are the official Stripe documentation links for their UI components and payments APIs:
1. Stripe Elements (Custom Forms)
2. Stripe Checkout (Hosted Page)
3. Stripe.js (Front-end SDK)
4. Stripe API Reference
5. Stripe React Integration
Bookmark: