·3 min read

Building an E-Commerce Site with Next.js and Stripe — The Parts Nobody Covers

Stripe's docs are great. The edge cases that break your checkout flow? Not documented. Real lessons from building an online store.

Next.jsStripeE-CommerceWeb Dev

Every tutorial shows you the happy path: create a Checkout Session, redirect, done. Nobody shows you what happens when a webhook fails silently, a payment succeeds but your database doesn't update, or a customer refreshes the page mid-checkout.

I built an e-commerce store for a client selling handmade wire bouquets. Here's what the tutorials skip.

The basic setup works great

Stripe Checkout is genuinely well-designed. Creating a session is straightforward:

const session = await stripe.checkout.sessions.create({
  payment_method_types: ["card"],
  line_items: cartItems.map((item) => ({
    price_data: {
      currency: "usd",
      product_data: { name: item.name },
      unit_amount: item.price * 100, // Stripe uses cents
    },
    quantity: item.quantity,
  })),
  mode: "payment",
  success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${origin}/cart`,
});

This works. The checkout page looks professional, handles cards, Apple Pay, Google Pay. But this is where the tutorials stop and the real problems begin.

Webhook handling — where things go wrong

The success_url redirect is for the user. The actual order confirmation must come from webhooks. Here's why: users can close the browser tab before the redirect. They can lose internet. The redirect is not reliable.

// This runs on your server, not the browser
export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get("stripe-signature")!;
 
  // ALWAYS verify the signature
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET!
  );
 
  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    // Fulfill the order here
    await fulfillOrder(session);
  }
 
  return new Response(null, { status: 200 });
}

Critical: Always return a 200 response quickly. Stripe retries webhooks that don't get a 200, which means you might process the same order twice. Use idempotency keys — check if you've already processed this event ID before fulfilling.

The inventory race condition

Here's a fun one: two people buy the same last item at the same time. Both checkout sessions succeed. Both webhooks fire. You now have two orders for one item.

Solutions:

  1. Reserve inventory when creating the Checkout Session (set a 30-minute TTL)
  2. Check inventory again in the webhook handler before confirming
  3. Use database transactions with row-level locking

Failed payment recovery

Cards get declined. Checkout sessions expire after 24 hours. You need a strategy:

  • Store the cart in your database with the session ID
  • If the session expires, email the customer with a new checkout link
  • Track abandoned checkouts — they're a huge revenue opportunity

The order confirmation email

Stripe doesn't send order emails. You need to:

  1. Listen for checkout.session.completed
  2. Generate a nice HTML email with order details
  3. Send it via your email provider (I use Nodemailer)

Don't skip this. Customers expect confirmation. If they don't get one, they'll think the payment failed and try again.

Security checklist

  • Webhook signature verification — never skip this
  • Idempotency on order creation — prevent duplicates from retries
  • Rate limit your checkout endpoint — prevent abuse
  • Never trust client-side prices — always calculate server-side
  • Store Stripe IDs, not amounts — use Stripe as the source of truth

That last one is important. Don't store $49.99 in your database. Store the Stripe Price ID and look up the amount from Stripe. This prevents price manipulation.

The parts that took longer than expected

  • Shipping integration — calculating rates, tracking numbers, updating order status
  • Inventory sync — keeping stock counts accurate across webhooks and admin edits
  • Image optimization — product photos are huge. Use Next.js <Image> with proper sizing.
  • SEO for products — each product page needs its own metadata, structured data, and OG image

The Stripe integration itself took two days. The rest of the store took three weeks.

Want an e-commerce site that works?

I build online stores with Next.js and Stripe — properly, with webhooks, inventory management, and email confirmations from day one. Let's talk.