Shopware Store API mit Next.js: Headless-Frontend Schritt für Schritt anbinden
Das Storefront ist zu starr, Theme-Anpassungen kosten unverhältnismäßig viel Zeit, und das Designteam wartet auf Freiheiten, die Twig nicht hergibt. Der nächste logische Schritt: ein entkoppeltes Frontend auf Basis von Next.js, das die Shopware Store API als Datenquelle nutzt. Wie das konkret aussieht — von der ersten API-Anfrage bis zum funktionierenden Warenkorb — zeige ich hier.
1. Store API vs. Admin API: Der richtige Einstiegspunkt
Shopware 6 hat zwei separate API-Ebenen. Die Admin API ist für Backend-Operationen gedacht: Produkte anlegen, Bestellungen verwalten, Konfigurationen ändern. Sie erfordert OAuth-Authentifizierung und ist nicht für den Einsatz im Browser geeignet.
Das Headless-Frontend kommuniziert ausschließlich mit der Store API (erreichbar unter /store-api/). Sie ist öffentlich zugänglich, auf den Sales Channel beschränkt und gibt exakt die Daten zurück, die ein Shop-Frontend braucht: Produkte, Kategorien, Preise, Warenkorb, Checkout. Kein Umweg über die Admin-Ebene, kein Token-Handling im Browser.
Der einzige Pflicht-Header für jeden Request ist der sw-access-key — den Sales-Channel-Access-Key, den du im Shopware-Backend unter Einstellungen → Sales Channels findest.
2. Session-Handling: Den sw-context-token richtig nutzen
Der erste Fallstrick für alle, die neu mit der Store API arbeiten: Shopware kennt keinen klassischen Cookie-basierten Login-State. Stattdessen gibt jeder API-Call, der eine neue Session initiiert, einen sw-context-token im Response-Header zurück.
Dieser Token identifiziert die Session — Warenkorb, Kundenlogin, Currency-Einstellungen. Du musst ihn clientseitig speichern (z.B. im localStorage oder als Cookie) und bei jedem folgenden Request im Header mitschicken. Machst du das nicht, sprichst du bei jedem Call gegen eine leere, neue Session.
Beispiel: Zentrales Fetch-Utility mit Context-Token
const BASE_URL = process.env.NEXT_PUBLIC_SHOPWARE_URL;
const ACCESS_KEY = process.env.NEXT_PUBLIC_SHOPWARE_ACCESS_KEY;
export async function storeFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const contextToken =
typeof window !== "undefined"
? localStorage.getItem("sw-context-token")
: null;
const res = await fetch(`${BASE_URL}/store-api${path}`, {
...options,
headers: {
"Content-Type": "application/json",
"sw-access-key": ACCESS_KEY!,
...(contextToken ? { "sw-context-token": contextToken } : {}),
...options.headers,
},
});
const newToken = res.headers.get("sw-context-token");
if (newToken && typeof window !== "undefined") {
localStorage.setItem("sw-context-token", newToken);
}
return res.json();
}Dieses zentrale Utility macht den Token transparent für alle anderen Funktionen im Projekt. Du rufst es auf — es kümmert sich um das Session-Handling.
3. Produkte und Kategorien laden
Die Store API arbeitet durchgängig mit POST-Requests und einem JSON-Body, der Filter, Sortierung und Assoziationen steuert. Das ist anfangs ungewohnt — GET-Requests mit Query-Parametern gibt es kaum. Der Vorteil: Du bekommst exakt die Felder zurück, die du anforderst, nichts weiter.
Produktliste für eine Kategorie laden
// In einer Next.js Server Component (App Router)
async function getProductsByCategory(categoryId: string) {
const data = await storeFetch<ProductListResponse>(
"/product-listing/" + categoryId,
{
method: "POST",
body: JSON.stringify({
limit: 24,
includes: {
product: ["id", "name", "cover", "calculatedPrice", "seoUrls"],
product_media: ["media"],
media: ["url", "alt"],
},
}),
}
);
return data.elements;
}Der includes-Parameter ist entscheidend für die Performance. Ohne ihn liefert Shopware alle verfügbaren Felder zurück — das kann schnell mehrere Kilobyte pro Produkt werden. Mit gezielten Includes reduzierst du die Payload erheblich.
4. Warenkorb: Hinzufügen, Aktualisieren, Auschecken
Der Warenkorb läuft vollständig über den Context-Token — kein separates State-Management auf dem Server notwendig. Ein Produkt hinzufügen ist ein einzelner POST:
Produkt in den Warenkorb legen
export async function addToCart(productId: string, quantity = 1) {
return storeFetch("/checkout/cart/line-item", {
method: "POST",
body: JSON.stringify({
items: [
{
id: crypto.randomUUID(),
type: "product",
referencedId: productId,
quantity,
},
],
}),
});
}Den aktuellen Warenkorbinhalt holst du über GET /store-api/checkout/cart. Die Response enthält Positionen, berechnete Preise, angewandte Promotions und den Gesamtbetrag — alles bereits für die Anzeige aufbereitet, inklusive Steuern.
Für den Checkout-Prozess selbst empfiehlt sich ein zweistufiges Modell: Adresse und Zahlungsmethode über die Store API setzen, dann den Order-Vorgang starten. Der komplette Ablauf läuft über fünf bis sechs Endpunkte, die Shopware sauber dokumentiert.
5. Static Generation vs. Server-Side Rendering
Next.js mit App Router gibt dir die Wahl, und beide Strategien haben ihren Platz in einem Headless-Shopware-Setup:
- Static Generation (SSG)Ideal für Produktdetailseiten und Kategorie-Übersichten. Du generierst alle Seiten zur Build-Zeit aus der Store API, das Ergebnis liegt als statisches HTML vor — maximale Performance, kein Server-Overhead zur Laufzeit. Neue Produkte erfordern einen Revalidierungsmechanismus, zum Beispiel über ISR (Incremental Static Regeneration) mit einem revalidate-Intervall oder über Shopware-Webhooks, die einen On-Demand-Revalidate-Endpoint triggern.
- Server-Side Rendering (SSR)Notwendig für alles, was vom Nutzer oder dessen Session abhängt: Warenkorb-Ansicht, Kundenaccount, personalisierte Preise. Diese Seiten werden zur Laufzeit gerendert, damit der aktuelle Context-Token berücksichtigt werden kann.
In der Praxis nutzt du beide Modi im selben Next.js-Projekt: statische Produktseiten für die Breite, dynamische Seiten für alles rund um den Nutzer-State.
6. Häufige Fallstricke aus der Praxis
Ein paar Probleme begegnen mir in fast jedem Headless-Shopware-Projekt, die ich hier direkt adressiere:
- CORS-Fehler im BrowserDie Store API blockiert standardmäßig alle Origins außer der konfigurierten Storefront-URL. Für das Headless-Frontend musst du im Shopware-Backend unter Sales Channels → Allowed origins die Domain deines Next.js-Frontends explizit eintragen.
- SEO-URLs nicht auflösbarShopware verwaltet SEO-URLs intern. Wenn du Produktseiten unter deren SEO-Slug erreichbar machen willst (statt unter
/product/[id]), musst du den EndpunktPOST /store-api/seo-urlnutzen, um den Slug zur internen ID aufzulösen, bevor du die Produktdaten lädst. - Preise für B2B-KundenKundenspezifische Preise und Kundengruppen-Preise werden von der Store API korrekt ausgeliefert — aber erst, nachdem sich der Nutzer eingeloggt hat. Für anonyme Sessions liefert Shopware immer den Listenpreis. Das ist im B2B-Kontext oft eine Anforderung, die früh im Architekturdesign bedacht werden muss.
7. Wann lohnt sich der Aufwand — und wann nicht?
Ein Headless-Setup mit der Store API ist kein Selbstzweck. Der Aufwand ist real: kein fertiges Theme, kein Plugin, das einfach ins Storefront-UI schreibt, kein visueller Page Builder out of the box. Du baust das Frontend from scratch.
Es rechnet sich, wenn das Designteam vollständige Freiheit über das Frontend braucht, wenn Performance-Anforderungen über das hinausgehen, was das Storefront leisten kann, oder wenn das Frontend in einen bestehenden Tech-Stack (z.B. ein unternehmensweites Design-System in React) integriert werden muss.
Für einen Standard-Shop mit gelegentlichen Theme-Anpassungen und überschaubaren B2C-Anforderungen ist das native Shopware-Storefront meist die bessere Wahl — schneller live, einfacher zu betreiben, und der Plugin-Markt greift vollständig.
Headless-Projekt geplant?
Ich helfe dir dabei, die richtige Architektur zu wählen und die Store API sauber in dein Next.js-Frontend zu integrieren — ob als technischer Lead oder als Sparringspartner für dein Team.