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

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:
- Frontend: Nutzer klickt „Zur Zahlung“, ruft Ihre API für Checkout Session auf
- Backend: Session erstellen,
session.idzurückgeben - Frontend: Mit Stripe.js zur gehosteten Zahlungsseite weiterleiten
- Nutzer: Kreditkartendaten auf Stripe-Seite eingeben, zahlen
- Stripe: Nach Erfolg Webhook an Ihr Backend senden
- Backend: Webhook empfangen, Bestellung anlegen, Lager reduzieren, E-Mail senden
- 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_amountmit 100 multiplizieren – Stripe rechnet in Cent (99,99 USD = 9999 Cent){CHECKOUT_SESSION_ID}insuccess_urlwird von Stripe durch echtesession_idersetztmetadatafü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:
- bodyParser deaktivieren: Stripe braucht Raw Body für Signaturprüfung – vorzeitiges Parsing führt zu Fehlern
- Signatur prüfen:
stripe.webhooks.constructEvent()bestätigt Stripe-Herkunft, verhindert Fälschungen - Idempotenz: Stripe kann Webhooks wiederholen –
stripeSessionIdals 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
- Produktseite: „In den Warenkorb“ – Zustand-Store aktualisiert, Icon-Zähler +1
- Warenkorb: Menge anpassen, „Zur Kasse“
- Checkout: Bestellübersicht, „Zur Zahlung“
- Frontend:
/api/create-checkoutmit Warenkorbdaten - Backend: Stripe Session,
sessionIdzurück - Frontend: Weiterleitung zur Stripe-Zahlungsseite
- Nutzer: Karte eingeben, „Pay“
- Stripe: Zahlung, Webhook an
/api/stripe-webhook - Backend-Webhook: Signatur → Bestellung → Lager → E-Mail
- Stripe: Redirect zu
/success?session_id=xxx - 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:
- Stripe Dashboard öffnen
- „Developers“ → „Webhooks“
- „Add endpoint“
- URL:
https://yourdomain.com/api/stripe-webhook - Events:
checkout.session.completed,payment_intent.succeededusw. - 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 (
stripeSessionIdeindeutig) - ✅ 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:
- Warenkorb: Zustand (leicht) oder Redux Toolkit (große Projekte), persist für Persistenz
- Zahlung: Stripe Checkout Session, gehostete Seite – kein Formular-Bau
- Bestellung: Webhook für Anlegen, Lager, E-Mail – zuverlässig
- 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
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
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
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
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
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?
• 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?
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ä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?
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?
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
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js E2E-Tests: Praxisleitfaden für automatisierte Tests mit Playwright
Vom manuellen Testen zu automatisierten E2E-Tests: Playwright-Konfiguration, Page Object Model, API-Tests und CI/CD-Integration – mit konkreten Praxisbeispielen.
Teil 36 von 51
Nächster
Next.js Datei-Upload: S3/Qiniu mit presigned URLs – Praxisguide
Lernen Sie, wie Sie in Next.js mit presigned URLs direkt zu S3 oder Qiniu hochladen – 4-MB-Limit umgehen, Dateien bis 5 GB, mit Codebeispielen, Performance-Tipps und Best Practices für Produktion.
Teil 38 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen