How to Add Stripe to a Next.js 15 App Router Project
Server Actions, Route Handlers, and the runtime gotcha that breaks most first attempts.
Zeeshan
Founder · Published
Server Actions, Route Handlers, and the runtime gotcha that breaks most first attempts.
Zeeshan
Founder · Published
App Router changes the shape of a Stripe integration more than most teams expect. The old "API routes" mental model still mostly works, but two things trip up almost every first attempt: Server Actions for creating sessions, and the runtime configuration on your webhook handler.
A Server Action is the cleanest way to create a Checkout Session from a form submission: no separate API route needed for the simple case.
"use server";
export async function createCheckoutSession() {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: "price_123", quantity: 1 }],
success_url: `${process.env.SITE_URL}/success`,
cancel_url: `${process.env.SITE_URL}/cancel`,
});
redirect(session.url!);
}Webhook signature verification needs the raw request body and Node.js APIs. It will not work on the Edge runtime. Explicitly force the Node runtime on your webhook route.
export const runtime = "nodejs";
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
// handle event
}Miss this and signature verification fails intermittently in production while working fine locally: a classic case of an environment difference masking the real bug.
Stripe insights, monthly
One email a month, no spam, unsubscribe anytime.
No spam, unsubscribe anytime.
Let Stripe own the billing state and Django own the entitlement. Mixing the two is where it goes wrong.
The raw-body problem bites PHP integrations harder than most. Here's the correct shape.
The Elements provider, the client secret lifecycle, and the re-render bug that breaks most first attempts.