Design wechseln

Next.js E-Commerce in der Praxis: Warenkorb und Stripe-Zahlung – vollständige Implementierung

Easton editorial illustration: build pipeline conveyor

Beim 27. Mal die Stripe-Webhook-Konfiguration prüfen – Nutzer beschweren sich, dass das Geld abgebucht wurde, der Bestellstatus aber noch „Zahlung ausstehend“ ist. In der Testumgebung lief alles reibungslos. Warum scheitert es in Produktion?

Als ich mein erstes Next.js-E-Commerce-Projekt gebaut habe, dachte ich, die größte Hürde sei UI und Styling. In Wahrheit lauern bei Warenkorb-State-Management, Zahlungsanbindung und Bestellablauf in jedem Schritt Fallstricke. Redux wirkte zu schwer, Context API hatte Performance-Probleme, die Stripe-Dokumentation war auf Englisch überwältigend – und Webhooks? Keine Ahnung.

Am frustrierendsten: Die meisten Tutorials behandeln entweder nur den Warenkorb oder nur die Zahlung. Selten wird der komplette Ablauf durchgängig erklärt. Vielleicht fragen Sie sich: „Welches State-Management soll ich wählen?“, „Wofür brauche ich Webhooks?“, „Wie synchronisiere ich Bestell- und Zahlungsstatus?“

In diesem Artikel füllen wir diese Lücken. Wir nutzen Zustand für den Warenkorb (leicht und praktisch), Stripe für Zahlungen (internationaler Standard) und Webhooks für Bestellungen (die einzige zuverlässige Methode). Jeder Schritt enthält vollständigen Code zum direkten Übernehmen.

Kennen Sie dieses Gefühl, wenn endlich alles funktioniert? Wenn Sie diesem Artikel folgen, werden Sie es erleben.

Warum Zustand für den Warenkorb?

State-Management-Auswahl 2025: Schluss mit dem Grübeln

Ehrlich gesagt ist die Wahl eines State-Management-Libraries mühsam. Redux-Dokumentation dick wie ein Wörterbuch, Context-API-Performance-Probleme überall, Zustand wirkt zu neu. Ich habe lange zwischen den drei Optionen gewechselt – bis mich eine Entwicklung überzeugte.

Seit 2021 wächst Zustand am schnellsten unter den React-State-Management-Libraries. Bis 2025 hat sich das Design bewährt: funktional, hooks-orientiert, elegante API. Die Lernkurve ist flach – anders als bei Redux mit action, reducer, dispatch, middleware …

Wie wählen Sie? Kurz zusammengefasst:

  • Kleine Projekte (<10 Seiten): Context API reicht – nicht over-engineeren
  • Mittlere Projekte (10–50 Seiten): Zustand – leicht und ausreichend
  • Große Projekte (50+ Seiten, mehrere Teams): Redux Toolkit – vollständige Toolchain

Für Warenkörbe passt Zustand gut: plattformübergreifendes Teilen (Produktliste, Warenkorb-Icon, Checkout), Persistenz (Daten nach Reload), Performance (nur betroffene Komponenten aktualisieren). Das schafft Zustand mit deutlich weniger Code als Redux.

Zustand-Warenkorb: Praxiscode

Genug Theorie – direkt zum Code. Zuerst die Abhängigkeit:

npm install zustand

Dann den Warenkorb-Store anlegen (/store/cartStore.js):

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useCartStore = create(
  persist(
    (set, get) => ({
      // Status
      items: [], // [{ id, name, price, quantity, image }]

      // Berechnete Eigenschaften
      get total() {
        return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0)
      },
      get count() {
        return get().items.reduce((sum, item) => sum + item.quantity, 0)
      },

      // Methoden
      addItem: (product) => set((state) => {
        const existing = state.items.find(item => item.id === product.id)
        if (existing) {
          // Bereits vorhanden – Menge +1
          return {
            items: state.items.map(item =>
              item.id === product.id
                ? { ...item, quantity: item.quantity + 1 }
                : item
            )
          }
        } else {
          // Neues Produkt hinzufügen
          return { items: [...state.items, { ...product, quantity: 1 }] }
        }
      }),

      removeItem: (productId) => set((state) => ({
        items: state.items.filter(item => item.id !== productId)
      })),

      updateQuantity: (productId, quantity) => set((state) => ({
        items: state.items.map(item =>
          item.id === productId ? { ...item, quantity } : item
        )
      })),

      clearCart: () => set({ items: [] })
    }),
    {
      name: 'shopping-cart', // localStorage-Schlüssel
    }
  )
)

Die Logik ist einfach: items speichert Produkte, total und count werden berechnet, Methoden für CRUD. Die persist-Middleware speichert automatisch in localStorage – Daten bleiben nach Reload erhalten.

In Komponenten ist die Nutzung unkompliziert:

import { useCartStore } from '@/store/cartStore'

function ProductCard({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <button onClick={() => addItem(product)}>
      In den Warenkorb
    </button>
  )
}

function CartIcon() {
  const count = useCartStore(state => state.count)

  return <div>Warenkorb ({count})</div>
}

useCartStore(state => state.addItem) ist ein Selektor – er abonniert nur addItem, Re-Renders bei anderen Warenkorb-Änderungen entfallen. Das ist Zustands Performance-Geheimnis: präzise Subscription.

Wer Redux mit useSelector und useDispatch kennt, wird Zustands Einfachheit schätzen: keine action types, keine reducer – Methoden direkt im Store.

„Mein Projekt nutzt bereits Redux – muss ich wechseln?“ Nein. Redux Toolkit funktioniert ebenfalls gut; das Open-Source-Projekt C-Shopping nutzt Redux Toolkit + RTK Query. Für neue Projekte empfehle ich persönlich Zustand – geringere Lernkurve, schneller produktiv.

Stripe-Zahlung: vollständiger Integrationsablauf

Den Stripe-Zahlungsablauf verstehen

Beim ersten Lesen der Stripe-Dokumentation war alles unklar: Was ist eine Checkout Session? Payment Intent? Warum Weiterleitung zu Stripe? Kann die Zahlung auf der eigenen Seite bleiben?

Rückblickend ist der Ablauf klar:

  1. Frontend: Nutzer klickt „Zur Zahlung“, ruft Ihre API für Checkout Session auf
  2. Backend: Session erstellen, session.id zurückgeben
  3. Frontend: Mit Stripe.js zur gehosteten Zahlungsseite weiterleiten
  4. Nutzer: Kreditkartendaten auf Stripe-Seite eingeben, zahlen
  5. Stripe: Nach Erfolg Webhook an Ihr Backend senden
  6. Backend: Webhook empfangen, Bestellung anlegen, Lager reduzieren, E-Mail senden
  7. Stripe: Nutzer zu Ihrer Website zurückleiten (success_url)

Wichtig: Zahlungserfolg niemals im Frontend verarbeiten. Nutzer schließen den Browser, Netzwerk bricht ab, oder sie springen absichtlich nicht weiter. Die einzige zuverlässige Quelle ist der Webhook – dazu gleich mehr.

Stripe Checkout Session erstellen

Abhängigkeiten installieren:

npm install stripe @stripe/stripe-js

Umgebungsvariablen (.env.local):

STRIPE_SECRET_KEY=sk_test_xxxxx  # Backend – niemals ins Frontend
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx  # Frontend
STRIPE_WEBHOOK_SECRET=whsec_xxxxx  # Webhook-Signaturprüfung

API-Route (/pages/api/create-checkout.js):

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { items } = req.body // Warenkorbdaten

    // line_items im Stripe-Format
    const lineItems = items.map(item => ({
      price_data: {
        currency: 'usd',
        product_data: {
          name: item.name,
          images: [item.image],
        },
        unit_amount: Math.round(item.price * 100), // Stripe rechnet in Cent
      },
      quantity: item.quantity,
    }))

    // Checkout Session erstellen
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: lineItems,
      mode: 'payment', // Einmalzahlung (Abo: 'subscription')
      success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/cart`,
      metadata: {
        // Eigene Daten – im Webhook abrufbar
        userId: req.user?.id || 'guest',
      },
    })

    res.status(200).json({ sessionId: session.id })
  } catch (err) {
    console.error('Checkout Session erstellen fehlgeschlagen:', err)
    res.status(500).json({ error: err.message })
  }
}

Details:

  • unit_amount mit 100 multiplizieren – Stripe rechnet in Cent (99,99 USD = 9999 Cent)
  • {CHECKOUT_SESSION_ID} in success_url wird von Stripe durch echte session_id ersetzt
  • metadata für eigene Daten (Nutzer-ID, Notizen) – im Webhook verfügbar

Checkout im Frontend aufrufen

Checkout-Seite (/pages/checkout.js):

import { loadStripe } from '@stripe/stripe-js'
import { useCartStore } from '@/store/cartStore'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)

export default function CheckoutPage() {
  const { items, total } = useCartStore()

  const handleCheckout = async () => {
    try {
      // Backend-API für Session
      const response = await fetch('/api/create-checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items }),
      })

      const { sessionId } = await response.json()

      // Weiterleitung zur Stripe-Zahlungsseite
      const stripe = await stripePromise
      const { error } = await stripe.redirectToCheckout({ sessionId })

      if (error) {
        console.error('Weiterleitung zur Zahlungsseite fehlgeschlagen:', error)
        alert(error.message)
      }
    } catch (err) {
      console.error('Zahlung starten fehlgeschlagen:', err)
      alert('Zahlung fehlgeschlagen, bitte später erneut versuchen')
    }
  }

  return (
    <div>
      <h1>Checkout</h1>
      {items.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity} = ${item.price * item.quantity}
        </div>
      ))}
      <div>Gesamt: ${total}</div>
      <button onClick={handleCheckout}>Zur Zahlung</button>
    </div>
  )
}

Nach Klick auf „Zur Zahlung“ landen Nutzer auf der Stripe-gehosteten Seite – Formular, Kartenvalidierung und Betrugsprüfung übernimmt Stripe.

„Kann ich das Design anpassen?“ Ja – Farben, Logo, Schrift. Das Layout ist fest. Für vollständige UI-Kontrolle gibt es Stripe Elements (eingebettetes Formular) – deutlich komplexer, für Einsteiger weniger empfehlenswert.

Weiterleitung nach erfolgreicher Zahlung

Nach Zahlung leitet Stripe zu success_url weiter. Dort Bestelldetails anzeigen:

// /pages/success.js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'

export default function SuccessPage() {
  const router = useRouter()
  const { session_id } = router.query
  const [order, setOrder] = useState(null)

  useEffect(() => {
    if (session_id) {
      // Bestellinfo vom Backend
      fetch(`/api/order?session_id=${session_id}`)
        .then(res => res.json())
        .then(data => setOrder(data))
    }
  }, [session_id])

  if (!order) return <div>Laden …</div>

  return (
    <div>
      <h1>Zahlung erfolgreich!</h1>
      <p>Bestellnummer: {order.id}</p>
      <p>Betrag: ${order.total}</p>
    </div>
  )
}

Diese Seite ist nur zur Anzeige – die echte Bestellung entsteht im Webhook. Als Nächstes: Webhook-Implementierung.

Webhook: Bestellverarbeitung und Status-Synchronisation

Warum Webhooks so wichtig sind

Beim ersten Mal dachte ich naiv: Zurück auf der Success-Seite = Zahlung erfolgreich – die gesamte Bestelllogik lag dort. Beim Test schlossen Nutzer den Browser nach der Zahlung – keine Bestellung, unklar ob Geld zurückkam.

Stripe-Dokumentation klärt: Webhook ist die einzige zuverlässige Bestellverarbeitung.

  • Nutzer-Weiterleitung unzuverlässig: Browser schließen, Netzwerk, kein Klick auf Abschluss
  • Sicherheit: Bestellung, Lager, Versand gehören ins Backend, nicht ins Frontend
  • Stripe-Empfehlung: Kritische Logik in Webhooks

Ein Webhook bedeutet: Stripe ruft Ihren Server aktiv auf – „Zahlung erfolgreich“ oder „Abo gekündigt“. Sie reagieren entsprechend.

Webhook-Endpunkt erstellen

In Next.js /pages/api/stripe-webhook.js:

import Stripe from 'stripe'
import { buffer } from 'micro'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET

// Wichtig: Next.js Standard-Body-Parsing deaktivieren
export const config = {
  api: {
    bodyParser: false,
  },
}

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).send('Method not allowed')
  }

  const buf = await buffer(req)
  const sig = req.headers['stripe-signature']

  let event

  try {
    // Webhook-Signatur prüfen (kritisch!)
    event = stripe.webhooks.constructEvent(buf, sig, webhookSecret)
  } catch (err) {
    console.error('Webhook-Signaturprüfung fehlgeschlagen:', err.message)
    return res.status(400).send(`Webhook Error: ${err.message}`)
  }

  // Event-Typen verarbeiten
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutSessionCompleted(event.data.object)
      break
    case 'payment_intent.succeeded':
      await handlePaymentIntentSucceeded(event.data.object)
      break
    case 'invoice.payment_failed':
      await handleInvoicePaymentFailed(event.data.object)
      break
    default:
      console.log(`Nicht verarbeiteter Event-Typ: ${event.type}`)
  }

  res.status(200).json({ received: true })
}

async function handleCheckoutSessionCompleted(session) {
  console.log('Zahlung erfolgreich!', session.id)

  const userId = session.metadata.userId
  const sessionId = session.id
  const total = session.amount_total / 100 // zurück in USD

  // Idempotenz: Bestellung bereits vorhanden?
  const existingOrder = await db.order.findUnique({
    where: { stripeSessionId: sessionId }
  })

  if (existingOrder) {
    console.log('Bestellung existiert bereits, Erstellung übersprungen')
    return
  }

  // Bestellung anlegen
  const order = await db.order.create({
    data: {
      userId,
      stripeSessionId: sessionId,
      status: 'paid',
      total,
      // ... weitere Felder
    }
  })

  // Lagerbestand reduzieren
  await updateInventory(order.items)

  // Bestätigungs-E-Mail
  await sendOrderConfirmationEmail(userId, order)

  console.log('Bestellung erfolgreich erstellt:', order.id)
}

async function handlePaymentIntentSucceeded(paymentIntent) {
  console.log('Zahlung bestätigt:', paymentIntent.id)
}

async function handleInvoicePaymentFailed(invoice) {
  console.log('Zahlung fehlgeschlagen:', invoice.id)
  // Erinnerungs-E-Mail, Dienst pausieren usw.
}

Kernpunkte:

  1. bodyParser deaktivieren: Stripe braucht Raw Body für Signaturprüfung – vorzeitiges Parsing führt zu Fehlern
  2. Signatur prüfen: stripe.webhooks.constructEvent() bestätigt Stripe-Herkunft, verhindert Fälschungen
  3. Idempotenz: Stripe kann Webhooks wiederholen – stripeSessionId als eindeutiger Index verhindert doppelte Bestellungen

Webhook lokal testen

Stripe erreicht localhost nicht direkt – lokal brauchen Sie die Stripe CLI:

# Mac
brew install stripe/stripe-cli/stripe

# Windows (Scoop)
scoop install stripe

# Oder von der Website
# https://stripe.com/docs/stripe-cli

Anmelden und Webhook weiterleiten:

stripe login
stripe listen --forward-to localhost:3000/api/stripe-webhook

Die CLI liefert ein temporäres webhook secret (whsec_xxxxx) – in .env.local eintragen:

STRIPE_WEBHOOK_SECRET=whsec_xxxxx

Test-Event auslösen:

stripe trigger checkout.session.completed

Logs in CLI und Next.js bestätigen den Empfang – dann Bestelllogik debuggen.

Ich hing hier lange fest – Signaturprüfung schlug fehl, weil bodyParser aktiv war. export const config nicht vergessen!

Bestellstatus-Verwaltung

Typischer Statusfluss:

Zahlung ausstehend → Bezahlt → In Vorbereitung → Versendet → Abgeschlossen

   Storniert/Erstattet

In der Datenbank als Enum:

// schema.prisma
model Order {
  id               String   @id @default(cuid())
  stripeSessionId  String   @unique  // Idempotenz
  userId           String
  status           OrderStatus @default(PENDING)
  total            Float
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
}

enum OrderStatus {
  PENDING      // Zahlung ausstehend
  PAID         // Bezahlt
  PREPARING    // In Vorbereitung
  SHIPPED      // Versendet
  COMPLETED    // Abgeschlossen
  CANCELLED    // Storniert
  REFUNDED     // Erstattet
}

Bei checkout.session.completed Status auf PAID setzen. Versand und Abschluss aktualisiert Ihr Admin-System manuell oder automatisch.

Fehlerbehandlung

Webhooks können fehlschlagen (Datenbank, Drittanbieter). Stripe retried – trotzdem Fehler loggen:

async function handleCheckoutSessionCompleted(session) {
  try {
    // Geschäftslogik
  } catch (error) {
    console.error('Bestellverarbeitung fehlgeschlagen:', error)
    await logError({
      type: 'webhook_error',
      event: 'checkout.session.completed',
      sessionId: session.id,
      error: error.message,
    })
    throw error // Stripe erkennt Fehler und retried
  }
}

Bei Webhook-Fehler retried Stripe 3 Tage. Im Stripe Dashboard fehlgeschlagene Webhooks manuell erneut senden.

Vollständiger Bestellablauf in der Praxis

Alle Bausteine sind da – so läuft der komplette Ablauf:

Der Weg vom Klick zur Bestellung

  1. Produktseite: „In den Warenkorb“ – Zustand-Store aktualisiert, Icon-Zähler +1
  2. Warenkorb: Menge anpassen, „Zur Kasse“
  3. Checkout: Bestellübersicht, „Zur Zahlung“
  4. Frontend: /api/create-checkout mit Warenkorbdaten
  5. Backend: Stripe Session, sessionId zurück
  6. Frontend: Weiterleitung zur Stripe-Zahlungsseite
  7. Nutzer: Karte eingeben, „Pay“
  8. Stripe: Zahlung, Webhook an /api/stripe-webhook
  9. Backend-Webhook: Signatur → Bestellung → Lager → E-Mail
  10. Stripe: Redirect zu /success?session_id=xxx
  11. Frontend: Success-Seite ruft /api/order?session_id=xxx, zeigt Details

Der Ablauf wirkt komplex, jeder Schritt ist klar. Schritt 9 muss im Webhook passieren – nicht Schritt 11.

Datenbankdesign

model Order {
  id               String      @id @default(cuid())
  stripeSessionId  String      @unique  // Idempotenz
  userId           String
  status           OrderStatus @default(PENDING)
  total            Float
  items            OrderItem[]
  createdAt        DateTime    @default(now())
  updatedAt        DateTime    @updatedAt

  user User @relation(fields: [userId], references: [id])
}

model OrderItem {
  id        String @id @default(cuid())
  orderId   String
  productId String
  quantity  Int
  price     Float  // Preis zum Bestellzeitpunkt – unabhängig von späteren Preisänderungen

  order   Order   @relation(fields: [orderId], references: [id])
  product Product @relation(fields: [productId], references: [id])
}

OrderItem.price speichert den Preis bei Bestellung, nicht den aktuellen Produktpreis – historische Bestellungen bleiben korrekt, auch bei späteren Preiserhöhungen.

Grenzfälle

1. Lagerbestand nicht ausreichend?

Vor Checkout Session prüfen:

// /pages/api/create-checkout.js
const { items } = req.body

for (const item of items) {
  const product = await db.product.findUnique({ where: { id: item.id } })
  if (product.stock < item.quantity) {
    return res.status(400).json({ error: `${product.name} – Lagerbestand nicht ausreichend` })
  }
}

// Lager OK – Session erstellen …

2. Zahlung erfolgreich, Webhook fehlgeschlagen?

Stripe retried 3 Tage. Manuell im Dashboard erneut senden. Oder Cronjob: Sessions mit Zahlung aber ohne Bestellung finden und nachbearbeiten.

3. Bezahlt, Versand verzögert?

Vor Versand Lager erneut prüfen. Bei Engpass: Erstattung oder Ersatz anbieten.

Produktions-Deployment: wichtige Hinweise

Testumgebung läuft – Produktion braucht Vorsicht. Typische Fallstricke:

Umgebungsvariablen

Produktionsschlüssel unterscheiden sich von Test:

# .env.production
STRIPE_SECRET_KEY=sk_live_xxxxx  # live, nicht test
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx  # Produktions-Webhook-Secret

Auf Vercel oder anderen Plattformen konfigurieren. Secret Key niemals in Git.

Webhook-Endpunkt

Lokal: Stripe CLI. Produktion: Stripe Dashboard:

  1. Stripe Dashboard öffnen
  2. „Developers“ → „Webhooks“
  3. „Add endpoint“
  4. URL: https://yourdomain.com/api/stripe-webhook
  5. Events: checkout.session.completed, payment_intent.succeeded usw.
  6. Signing secret kopieren (whsec_xxxxx) – in Umgebungsvariablen

Beim ersten Deployment vergaß ich das – Bestellungen blieben aus, Webhook kam nie an.

Sicherheits-Checkliste

Vor Go-Live durchgehen:

  • ✅ Gesamte Zahlungslogik im Backend (Frontend nur Weiterleitung)
  • ✅ Webhook-Signatur (stripe.webhooks.constructEvent)
  • ✅ Zahlungsbetrag mit Bestellbetrag abgleichen (Preismanipulation verhindern)
  • ✅ Idempotenz (stripeSessionId eindeutig)
  • ✅ Zahlungslogs (Sentry, Datadog)
  • ✅ Alerts (Webhook-Fehlerrate, Zahlungserfolgsrate)

Punkt 3 ist besonders wichtig: Auch wenn Session-Preise serverseitig gesetzt werden – im Webhook erneut validieren.

Monitoring und Alerts

Monitoring einbinden:

// /pages/api/stripe-webhook.js
import * as Sentry from '@sentry/nextjs'

export default async function handler(req, res) {
  try {
    // ... Webhook-Logik
  } catch (error) {
    Sentry.captureException(error, {
      tags: {
        type: 'stripe_webhook',
        event: event.type,
      },
    })
    throw error
  }
}

Metriken im Blick:

  • Webhook-Fehlerrate (>5 % alarmieren)
  • Zahlungserfolgsrate (plötzlicher Rückgang = Stripe oder Konfiguration)
  • Bestellerstellungsdauer (>3 Sekunden untersuchen)

Fazit: vom Nullpunkt bis Produktion

Kurz die Schlüsselschritte:

  1. Warenkorb: Zustand (leicht) oder Redux Toolkit (große Projekte), persist für Persistenz
  2. Zahlung: Stripe Checkout Session, gehostete Seite – kein Formular-Bau
  3. Bestellung: Webhook für Anlegen, Lager, E-Mail – zuverlässig
  4. Produktion: Umgebungsvariablen, Webhook-Endpunkt, Monitoring

Drei Prinzipien:

  • Zahlungslogik im Backend – Frontend ist nicht vertrauenswürdig
  • Webhook ist die einzige zuverlässige Quelle – Nutzer-Weiterleitung nicht
  • Sicherheit zuerst – Signatur, Replay-Schutz, Logs

Beim ersten E-Commerce-Projekt: Zuerst Stripe-Testumgebung durchspielen. Testkarte 4242 4242 4242 4242 (Ablauf und CVV beliebig). Erst dann Produktion.

Weiterführende Ressourcen:

  • Stripe-Dokumentation: https://stripe.com/docs
  • Next.js + Stripe: Pedro Alonsos Guide 2025 (suchen: „Stripe Next.js 15 complete guide“)
  • Open Source: C-Shopping mit Redux Toolkit + Stripe

Möge dieser Artikel einige Fallstricke ersparen. Wenn die erste Bestellung automatisch entsteht, Lager korrekt sinkt und die Bestätigungs-E-Mail ankommt – dieses Gefühl ist unschlagbar. Viel Erfolg!

Vollständiger Implementierungsablauf für Next.js-Warenkorb und Stripe-Zahlung

Schritt-für-Schritt-Aufbau eines E-Commerce-Warenkorbs und Zahlungssystems – State-Management, Zahlungsintegration und Bestellverarbeitung

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Abhängigkeiten installieren und Zustand-Warenkorb konfigurieren

    Zustand installieren:
    • npm install zustand

    Warenkorb-Store anlegen (/store/cartStore.js):
    • items-Array für Produkte definieren
    • Berechnete Eigenschaften total und count hinzufügen
    • Methoden addItem, removeItem, updateQuantity, clearCart implementieren
    • persist-Middleware für localStorage nutzen

    Wichtige Konfiguration:
    • persist speichert automatisch – Daten bleiben nach Seitenreload erhalten
    • Selektor-Subscription (useCartStore(state => state.addItem)) vermeidet unnötige Re-Renders

    Geeignet für: Mittlere Projekte (10–50 Seiten) mit leichtgewichtigem State-Management
  2. 2

    Step 2: Stripe Checkout Session API erstellen

    Stripe-Abhängigkeiten installieren:
    • npm install stripe @stripe/stripe-js

    Umgebungsvariablen konfigurieren (.env.local):
    • STRIPE_SECRET_KEY=sk_test_xxxxx (Backend, nicht preisgeben)
    • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx (Frontend)
    • STRIPE_WEBHOOK_SECRET=whsec_xxxxx (Webhook-Signaturprüfung)

    API-Route anlegen (/pages/api/create-checkout.js):
    • Warenkorb-items empfangen
    • In Stripe line_items konvertieren (unit_amount mit 100 multiplizieren)
    • checkout.sessions erstellen (success_url und cancel_url setzen)
    • Eigene Daten in metadata speichern (userId usw.)
    • sessionId ans Frontend zurückgeben

    Wichtige Details:
    • Stripe rechnet in Cent – Preis mit 100 multiplizieren
    • success_url mit Platzhalter {CHECKOUT_SESSION_ID}
    • metadata für Geschäftsdaten, im Webhook abrufbar
  3. 3

    Step 3: Stripe Checkout im Frontend aufrufen

    Checkout-Seite implementieren (/pages/checkout.js):
    • loadStripe für Stripe.js nutzen
    • /api/create-checkout für Session aufrufen
    • stripe.redirectToCheckout() zur Zahlungsseite

    Fehlerbehandlung:
    • Netzwerkfehler per catch abfangen
    • error von stripe.redirectToCheckout prüfen
    • Nutzerfreundliche Fehlermeldung anzeigen

    Zahlungsseite:
    • Stripe-gehostete Seite – kein eigenes Formular nötig
    • Kartenvalidierung und Betrugsprüfung automatisch
    • Farben, Logo und Schrift anpassbar

    Wichtig:
    • Zahlungserfolg niemals im Frontend verarbeiten
    • Nutzer kann Browser schließen oder Abschluss-Button nicht klicken
    • Bestellung muss im Webhook erstellt werden
  4. 4

    Step 4: Webhook-Endpunkt für Bestellverarbeitung konfigurieren

    Webhook-API anlegen (/pages/api/stripe-webhook.js):

    Pflichtkonfiguration:
    • export const config = { api: { bodyParser: false } } (Body-Parsing deaktivieren)
    • buffer(req) für Raw-Body nutzen
    • stripe.webhooks.constructEvent() für Signaturprüfung

    Event-Typen verarbeiten:
    • checkout.session.completed: Zahlung erfolgreich, Bestellung anlegen
    • payment_intent.succeeded: Zahlungseingang bestätigen
    • invoice.payment_failed: Abo-Zahlung fehlgeschlagen

    Idempotenz:
    • Prüfen, ob stripeSessionId bereits existiert
    • Eindeutigen Index in der Datenbank
    • Doppelte Webhook-Aufrufe verhindern

    Geschäftslogik:
    • Bestellung anlegen (status: 'paid')
    • Lagerbestand reduzieren (updateInventory)
    • Bestätigungs-E-Mail senden (sendOrderConfirmationEmail)
    • Logs und Fehler protokollieren

    Lokaler Test:
    • stripe login
    • stripe listen --forward-to localhost:3000/api/stripe-webhook
    • stripe trigger checkout.session.completed

    Kritisch:
    • bodyParser deaktivieren, sonst schlägt Signaturprüfung fehl
    • Signatur prüfen gegen gefälschte Anfragen
    • Bei Webhook-Fehler retried Stripe 3 Tage lang
  5. 5

    Step 5: Produktions-Deployment und Sicherheitskonfiguration

    Umgebungsvariablen:
    • Produktionsschlüssel (sk_live_xxxxx und pk_live_xxxxx)
    • Auf Plattform (Vercel/Netlify) konfigurieren
    • Secret Key niemals in Git committen

    Stripe Dashboard:
    • Developers → Webhooks
    • Produktions-Endpunkt (https://yourdomain.com/api/stripe-webhook)
    • Events auswählen (checkout.session.completed usw.)
    • Signing secret in Umgebungsvariablen

    Sicherheits-Checkliste:
    • ✅ Gesamte Zahlungslogik im Backend
    • ✅ Webhook-Signatur prüfen
    • ✅ Zahlungsbetrag mit Bestellbetrag abgleichen
    • ✅ Idempotenz (eindeutiger stripeSessionId-Index)
    • ✅ Alle Zahlungslogs protokollieren
    • ✅ Monitoring (Webhook-Fehlerrate >5 % alarmieren)

    Metriken:
    • Webhook-Fehlerrate
    • Zahlungserfolgsrate
    • Bestellerstellungsdauer (>3 Sekunden untersuchen)

    Monitoring:
    • Sentry/LogRocket für Fehler
    • Alert-Regeln konfigurieren
    • Stripe-Dashboard-Webhook-Logs regelmäßig prüfen

    Test:
    • Testkarte 4242 4242 4242 4242
    • Vollständiger Ablauf (Warenkorb → Zahlung → Webhook → Bestellung)
    • Fehlerszenarien (Lager leer, Webhook-Fehler)

FAQ

Redux oder Zustand – welche Wahl passt zu meinem Projekt?
Die Entscheidung hängt vor allem von Projektgröße und Team ab:

• Kleine Projekte (<10 Seiten): Context API reicht, kein zusätzliches State-Management nötig
• Mittlere Projekte (10–50 Seiten): Zustand – leichtgewichtig, geringe Lernkurve, wenig Code
• Große Projekte (50+ Seiten, mehrere Teams): Redux Toolkit – vollständige Toolchain, starke Debugging-Möglichkeiten, reife Community

Konkrete Szenarien:
• Neues Projekt, schnelle Iteration: Zustand – schneller Einstieg
• Bestehendes Redux-Projekt: nicht wechseln, Redux Toolkit funktioniert gut
• Team ohne State-Management-Erfahrung: Zustand hat flache Lernkurve

Für Warenkörbe empfehlen wir Zustand: plattformübergreifes Teilen, Persistenz und Performance-Optimierung.
Warum darf Zahlungserfolg nicht im Frontend verarbeitet werden?
Frontend-Verarbeitung hat gravierende Probleme:

Unzuverlässigkeit:
• Nutzer schließt Browser nach Zahlung
• Netzwerkabbruch verhindert Weiterleitung
• Nutzer klickt Abschluss-Button absichtlich nicht

Sicherheitsrisiken:
• Frontend-Code kann manipuliert werden
• Bestellung und Lagerbestand dürfen nicht im Frontend liegen
• Zahlungserfolg kann nicht zuverlässig fälschen werden verhindert

Richtiger Ansatz:
• Kritische Logik im Webhook
• Stripe benachrichtigt Backend direkt (ohne Browser)
• Webhook mit Signaturprüfung
• Stripe empfiehlt: Webhook ist die einzige zuverlässige Quelle

Success-Seite nur zur Anzeige – keine Geschäftslogik.
Webhook-Signaturprüfung schlägt ständig fehl – was tun?
Häufige Ursachen und Lösungen:

Häufigste Ursache (90 %):
• Next.js bodyParser nicht deaktiviert
• Lösung: export const config = { api: { bodyParser: false } } in der API-Route

Weitere Ursachen:
• Falscher Webhook Secret (STRIPE_WEBHOOK_SECRET in .env.local prüfen)
• Falsche Umgebung (Test- und Produktions-Secret unterscheiden sich)
• Body durch Middleware verändert (globale Body-Verarbeitung prüfen)

Debugging:
1. bodyParser: false bestätigen
2. req.headers['stripe-signature'] prüfen
3. Stripe CLI: stripe listen --forward-to localhost:3000/api/stripe-webhook
4. CLI-Fehlermeldungen lesen
5. Temporäres webhook secret aus CLI verwenden

Lokaler Test:
• Stripe CLI für Weiterleitung erforderlich
• CLI liefert temporäres secret (whsec_xxxxx)
• Bei CLI-Neustart neues secret in .env.local aktualisieren
Wie verhindere ich doppelte Bestellungen durch wiederholte Webhook-Aufrufe?
Idempotenz ist der Schlüssel:

Datenbankebene:
• Eindeutigen Index auf stripeSessionId
• Prisma-Beispiel: stripeSessionId String @unique
• Datenbank lehnt Duplikate ab

Code-Ebene:
• Vor Bestellung prüfen, ob bereits vorhanden
• findUnique({ where: { stripeSessionId } })
• Bei Existenz abbrechen, keine neue Bestellung

Beispielcode:
```javascript
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})

if (existingOrder) {
console.log('Bestellung existiert bereits, Erstellung übersprungen')
return
}

// Nur bei Nichtexistenz neue Bestellung anlegen
const order = await db.order.create({ ... })
```

Warum Idempotenz:
• Stripe kann Webhooks wiederholen (Netzwerk, Retry)
• Code muss Duplikate sicher verarbeiten
• Eine Zahlung darf nicht mehrere Bestellungen erzeugen

Weitere Tipps:
• Jeden Webhook-Aufruf loggen
• Häufigkeit wiederholter Aufrufe überwachen
• Alert-Mechanismus einrichten
Nach Produktions-Deployment werden keine Bestellungen erstellt – wie debuggen?
In dieser Reihenfolge prüfen:

1. Webhook-Empfang:
• Stripe Dashboard → Developers → Webhooks
• Aufrufhistorie und Status (Erfolg/Fehler)
• Keine Einträge = Endpunkt falsch konfiguriert

2. Endpunkt-Konfiguration:
• URL korrekt (https://yourdomain.com/api/stripe-webhook)
• Event checkout.session.completed ausgewählt
• Endpunkt aktiviert

3. Umgebungsvariablen:
• STRIPE_WEBHOOK_SECRET korrekt
• Produktions-secret (nicht Test)
• Auf Vercel/Netlify bestätigen

4. Webhook-Code:
• bodyParser deaktiviert
• Signaturprüfung korrekt
• Fehlerlogs vorhanden

5. Anwendungslogs:
• Server-Logs (Vercel Logs/CloudWatch)
• Fehler-Stack prüfen
• Webhook-Handler ausgeführt?

6. Manueller Test:
• Fehlgeschlagenen Webhook im Dashboard finden
• „Resend“ klicken
• Ergebnis und Fehlermeldung beobachten

Häufige Fehler:
• Webhook-Endpunkt in Produktion vergessen
• Test-webhook secret in Produktion
• Firewall blockiert Stripe-Anfragen

Nach Fix:
• Vollständigen Zahlungsablauf mit Testkarte
• Bestellung, Lagerbestand und E-Mail bestätigen

12 Min. Lesezeit · Veröffentlicht am: 7. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog