Typsichere Storefront-Anbindung mit Codegen
Magento liefert seinen GraphQL-Endpoint ohne Typinformationen fuer den Client, wodurch Storefront-Code auf gutes Glueck auf Feldnamen und Nullability vertraut. graphql-code-generator erzeugt aus dem echten Schema automatisch TypeScript-Typen und typisierte Query-Dokumente, sodass fehlerhafte Feldzugriffe und vergessene Null-Pruefungen schon beim Kompilieren auffallen statt erst im Checkout eines echten Kunden.
Inhaltsverzeichnis
- 1. Warum typsichere GraphQL-Anbindung fuer Headless-Magento-Storefronts zaehlt
- 2. Magentos GraphQL-Schema als Vertragsquelle: Introspection und schema.graphql
- 3. graphql-code-generator einrichten: codegen.yml gegen den Magento-Endpoint
- 4. Eine Produktlisten-Query typsicher vom Query-String bis zum Ergebnis
- 5. Warenkorb und Checkout typisieren: createEmptyCart, addProductsToCart, Cart-Query
- 6. Magentos nullable-lastiges Schema-Design im TypeScript-Alltag handhaben
- 7. Codegen in die Build-Pipeline integrieren: Schema-Aenderungen frueh erkennen
- 8. Caching und Performance bei typisierten GraphQL-Clients
- 9. Untypisierter Fetch-Aufruf vs. codegen-typisierte Query im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum typsichere GraphQL-Anbindung fuer Headless-Magento-Storefronts zaehlt
Ob PWA Studio, ein eigenes Next.js- oder Nuxt-Frontend, oder ein Hyvä-nahes JavaScript-Widget, das direkt gegen den Magento GraphQL-Endpoint spricht: Sobald ein Frontend nicht mehr innerhalb der Magento-Templates rendert, verschwindet die stillschweigende Konsistenzpruefung, die ein PHP-Block gegen sein eigenes Datenmodell automatisch mitliefert. Ein fetch()-Aufruf mit rohem GraphQL-String kennt weder die Feldnamen der Antwort noch deren Typen, jeder Zugriff auf data.products.items[0].price_range.minimum_price.regular_price.value ist reine Konvention, die der Compiler nicht prueft.
Genau hier setzt TypeScript in Kombination mit generierten Typen an: Statt Feldnamen aus der Erinnerung oder aus einer veralteten Dokumentation abzuschreiben, leitet ein Codegen-Werkzeug die exakten Typen direkt aus dem lebenden Schema ab. Tippfehler in Feldnamen, falsch angenommene Rueckgabetypen und vergessene Nullable-Faelle werden dadurch zu Compile-Fehlern statt zu undefined-Exceptions im Browser eines echten Kunden. Fuer Teams, die Magento als Backend und ein separates JavaScript-Frontend als Storefront betreiben, ist das kein Komfortfeature, sondern die Grundlage fuer verlaessliche Releases.
2. Magentos GraphQL-Schema als Vertragsquelle: Introspection und schema.graphql
Jeder typisierte Storefront-Client beginnt mit derselben Frage: Woher kommt die Wahrheit ueber Felder, Typen und Nullability? Die Antwort ist die GraphQL-Introspection, ein Standardmechanismus, mit dem sich jeder GraphQL-Endpoint selbst beschreibt. Gegen den Magento-Endpoint unter /graphql laesst sich per Introspection-Query oder ueber ein CLI-Werkzeug wie get-graphql-schema eine vollstaendige schema.graphql-Datei exportieren, die jeden Typ, jedes Feld und jede Nullability-Angabe des installierten Magento-Systems abbildet, inklusive eigener Erweiterungen aus Drittanbieter-Modulen.
Diese exportierte Schema-Datei ist der eigentliche Vertrag zwischen Backend und Frontend, nicht die offizielle Magento-Dokumentation, die bei individuellen Installationen mit eigenen schema.graphqls-Erweiterungen schnell veraltet. Wichtig ist, das Schema pro Umgebung neu zu exportieren, weil ein Custom-Modul, ein Magento-Upgrade oder ein deaktiviertes B2B-Modul die verfuegbaren Felder tatsaechlich veraendert. Das exportierte Schema dient anschliessend als Eingabe fuer den Codegen-Lauf und sollte versioniert im Repository liegen, damit Schema-Drifts im Diff sichtbar werden.
3. graphql-code-generator einrichten: codegen.yml gegen den Magento-Endpoint
graphql-code-generator ist das etablierte Werkzeug, um aus einem GraphQL-Schema und den im Projekt verwendeten Queries automatisch TypeScript-Typen zu erzeugen. Die Konfiguration liegt in einer codegen.yml, die entweder direkt auf den Magento-Endpoint oder auf die zuvor exportierte schema.graphql zeigt, gefolgt von einem Glob-Pattern, das alle .graphql-Dateien oder in Template-Literals eingebetteten Queries im Projekt einsammelt. Mit dem Plugin typescript-operations entstehen daraus Typen fuer jede einzelne Query und Mutation, mit typed-document-node zusaetzlich typisierte Dokumentobjekte, die von GraphQL-Clients wie Apollo oder urql ohne manuelle Typangabe konsumiert werden koennen.
Der generierte Code landet ueblicherweise in einer generated.ts, die niemals manuell editiert, sondern bei jedem Schema- oder Query-Wechsel neu erzeugt wird. In der Praxis laeuft graphql-codegen als npm-Skript, oft im --watch-Modus waehrend der Entwicklung, damit neu geschriebene Queries sofort typisierte Ergebnisse liefern, ohne dass der Entwickler manuell einen Build-Schritt anstossen muss.
# codegen.yml: points at the live Magento GraphQL endpoint
overwrite: true
schema:
- "https://shop.mironsoft.de/graphql":
headers:
Store: "default"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typed-document-node"
config:
# Magento's schema is nullable-heavy; keep that information explicit
avoidOptionals: false
skipTypename: false
scalars:
# Magento's Money/SortEnum scalars map cleanly to primitives here
SortEnum: "'ASC' | 'DESC'"
hooks:
afterOneFileWrite:
- "prettier --write"
4. Eine Produktlisten-Query typsicher vom Query-String bis zum Ergebnis
Eine typische Produktlisten-Query gegen products kombiniert Filter, Sortierung und Pagination in einer einzigen Anfrage und liefert verschachtelte Preisinformationen samt page_info zurueck. Statt diese Query als reinen String zu pflegen, wird sie im Projekt als benanntes .graphql-Dokument mit Fragmenten fuer wiederverwendbare Teile wie ProductCardFields abgelegt. Fragmente vermeiden Duplikation zwischen Listen- und Detailseiten und sorgen dafuer, dass ein neues Feld nur an einer Stelle ergaenzt werden muss, um in allen Verwendungsstellen typisiert zur Verfuegung zu stehen.
Nach dem Codegen-Lauf entsteht aus dieser Query automatisch ein Typenpaar: ein Eingabetyp fuer die Variablen, etwa ProductsQueryVariables, und ein Ausgabetyp fuer die Antwort, etwa ProductsQuery, beide exakt aus dem Schema abgeleitet. Der GraphQL-Client ruft die Query nicht mehr mit einem generischen gql-Tag und manuellem as ProductsQuery-Cast auf, sondern mit dem generierten TypedDocumentNode, das Eingabe- und Ausgabetyp automatisch an die Client-Methode weiterreicht. Ein Feldzugriff wie result.data.products.items ist damit vollstaendig typisiert, inklusive aller nullable Zwischenschritte.
# products.graphql: reusable fragment plus the actual list query
fragment ProductCardFields on ProductInterface {
id
sku
name
small_image {
url
label
}
price_range {
minimum_price {
regular_price {
value
currency
}
}
}
}
query Products($search: String!, $pageSize: Int!, $currentPage: Int!) {
products(search: $search, pageSize: $pageSize, currentPage: $currentPage) {
total_count
items {
...ProductCardFields
}
page_info {
current_page
page_size
total_pages
}
}
}
// generated.ts: excerpt of what graphql-code-generator produces for the query above
// Note how almost every field is nullable, mirroring Magento's schema exactly
export type ProductCardFieldsFragment = {
__typename?: 'SimpleProduct' | 'ConfigurableProduct';
id?: number | null;
sku?: string | null;
name?: string | null;
small_image?: { url?: string | null; label?: string | null } | null;
price_range?: {
minimum_price?: {
regular_price?: { value?: number | null; currency?: CurrencyEnum | null } | null;
} | null;
} | null;
};
export type ProductsQueryVariables = {
search: string;
pageSize: number;
currentPage: number;
};
export type ProductsQuery = {
products?: {
total_count?: number | null;
items?: Array<ProductCardFieldsFragment | null> | null;
page_info?: { current_page?: number | null; total_pages?: number | null } | null;
} | null;
};
// Typed document node: client methods infer variables and result automatically
export declare const ProductsDocument: TypedDocumentNode<ProductsQuery, ProductsQueryVariables>;
5. Warenkorb und Checkout typisieren: createEmptyCart, addProductsToCart, Cart-Query
Der Warenkorb-Flow ist der Bereich, in dem sich Magentos nullable-lastiges Schema am staerksten bemerkbar macht, weil hier mehrere Mutationen nacheinander aufgerufen werden und jede Antwort theoretisch user_errors statt der erwarteten Daten liefern kann. createEmptyCart liefert eine cart_id als String, addProductsToCart nimmt diese ID plus ein Array von CartItemInput-Objekten entgegen und liefert im Erfolgsfall den aktualisierten cart-Typ zurueck, im Fehlerfall aber ein user_errors-Array, das parallel zu den Cart-Daten existiert und ebenfalls geprueft werden muss.
Typsicher bedeutet hier konkret: Der generierte Typ fuer AddProductsToCartMutation markiert sowohl cart als auch die einzelnen Items als nullable, weil das Schema genau das zulaesst, auch wenn die Mutation in der Praxis fast immer entweder Daten oder Fehler liefert, nie beides leer. Ein sauberer Consuming-Code prueft deshalb zuerst user_errors.length, bevor er ueberhaupt auf cart zugreift, und behandelt einen leeren cart trotz fehlender Fehler als eigenen, expliziten Fehlerfall statt ihn mit einer Non-Null-Assertion zu uebergehen.
// Typed cart mutation with explicit null-safe handling of Magento's response shape
import { useMutation } from '@apollo/client';
import { AddProductsToCartDocument, type AddProductsToCartMutation } from '../generated/graphql';
interface AddToCartResult {
success: boolean;
itemCount: number;
errorMessage?: string;
}
async function addProductToCart(
cartId: string,
sku: string,
quantity: number,
): Promise<AddToCartResult> {
const { data, errors } = await client.mutate<AddProductsToCartMutation>({
mutation: AddProductsToCartDocument,
variables: { cartId, cartItems: [{ sku, quantity }] },
});
// Network/GraphQL-level errors first, separate from Magento's business errors
if (errors?.length) {
return { success: false, itemCount: 0, errorMessage: errors[0].message };
}
// Magento models domain errors as data, not as GraphQL errors
const userErrors = data?.addProductsToCart?.user_errors ?? [];
if (userErrors.length > 0) {
return { success: false, itemCount: 0, errorMessage: userErrors[0]?.message ?? 'Unknown cart error' };
}
// Even without user_errors, cart can technically be null per the schema
const cart = data?.addProductsToCart?.cart;
if (!cart) {
return { success: false, itemCount: 0, errorMessage: 'Cart response was empty' };
}
return { success: true, itemCount: cart.total_quantity ?? 0 };
}
6. Magentos nullable-lastiges Schema-Design im TypeScript-Alltag handhaben
Ein zentraler Kulturschock beim Umstieg von handgeschriebenen Interfaces auf codegen-generierte Typen: Magentos GraphQL-Schema markiert die meisten Felder als nullable, selbst dort, wo sie in der Praxis so gut wie immer vorhanden sind, etwa product.name oder cart.items. Das ist keine Schwaeche des Codegens, sondern eine korrekte Abbildung eines Schemas, das defensiv gegen Teilfehler, deaktivierte Module und asynchrone Datenverfuegbarkeit designt wurde. Der generierte Typ string | null | undefined zwingt jeden Verbraucher, diesen Fall bewusst zu behandeln, statt ihn zu ignorieren.
Drei Strategien haben sich in der Praxis etabliert. Die Non-Null-Assertion product!.name! ist am schnellsten geschrieben, verlagert das Risiko aber unveraendert in die Laufzeit und sollte nur an Stellen stehen, an denen ein fehlender Wert tatsaechlich einen Programmierfehler bedeutet. Optional Chaining mit sinnvollen Fallbacks, etwa product?.name ?? 'Unbenannt', ist sicherer, verteilt Fallback-Logik aber ueber die gesamte Codebasis. Am robustesten ist eine Normalisierungsschicht, die GraphQL-Rohdaten direkt nach dem Query-Aufruf in strikt nicht-nullable Domain-Typen ueberfuehrt und dabei explizit entscheidet, was bei fehlenden Daten passiert, statt diese Entscheidung an jede einzelne Komponente zu delegieren.
7. Codegen in die Build-Pipeline integrieren: Schema-Aenderungen frueh erkennen
Ein Magento-Upgrade, ein neu installiertes Drittanbieter-Modul oder eine eigene schema.graphqls-Erweiterung koennen Felder umbenennen, Typen aendern oder Nullability-Angaben verschaerfen, ohne dass der Storefront-Code davon automatisch erfaehrt. Wird graphql-codegen nur lokal und unregelmaessig ausgefuehrt, bleiben solche Schema-Drifts oft wochenlang unentdeckt, bis eine Komponente zur Laufzeit auf ein plötzlich fehlendes Feld trifft. Die Loesung ist, den Codegen-Lauf als festen CI-Schritt zu verankern, der bei jedem Pull Request gegen eine aktuelle Schema-Referenz laeuft.
Ein einfacher, aber wirksamer Check: Der CI-Job fuehrt graphql-codegen aus und vergleicht anschliessend das Diff der generierten graphql.ts gegen den eingecheckten Stand. Ein unerwartetes Diff bedeutet entweder eine bewusste Schema-Aenderung, die mitversioniert werden muss, oder eine ungewollte Drift, die vor dem Merge auffliegt statt in Produktion. Ergaenzend prueft tsc --noEmit im selben Job, ob bestehende Query-Verwendungen nach einer Schema-Aenderung noch kompilieren, was Breaking Changes in referenzierten Feldern zuverlaessig sichtbar macht, lange bevor ein Kunde eine kaputte Produktseite sieht.
8. Caching und Performance bei typisierten GraphQL-Clients
Typisierte Queries loesen ein Korrektheitsproblem, aber Performance bleibt eine eigene Dimension, die Codegen allein nicht abdeckt. Persisted Queries reduzieren die Netzwerklast, indem der Client statt des vollstaendigen Query-Strings nur einen Hash an den Magento-Endpoint sendet, den der Server gegen eine zuvor registrierte Query aufloest. Das spart insbesondere bei grossen, fragmentreichen Produktlisten-Queries mehrere Kilobyte pro Request und laesst sich gut mit generierten TypedDocumentNode-Objekten kombinieren, da deren Hash sich deterministisch aus dem Query-Dokument ableiten laesst.
Fragment-Kolokation ist die zweite wichtige Stellschraube: Statt eine grosse, zentrale Query mit allen jemals benoetigten Feldern zu pflegen, definiert jede Komponente ihr eigenes Fragment mit genau den Feldern, die sie tatsaechlich rendert. Ein GraphQL-Client wie Apollo kombiniert kolokierte Fragmente automatisch zu einer einzigen Netzwerkanfrage, wodurch Over-Fetching verschwindet, ohne dass Komponenten manuell koordinieren muessen, welche Felder bereits von anderer Stelle angefragt wurden. Codegen erzeugt fuer jedes Fragment einen eigenen Typ, sodass Komponenten weiterhin nur die Felder sehen, die sie selbst deklariert haben.
Der komplette Client-Layer bleibt dabei erstaunlich schlank: Ein einziger generischer Wrapper um fetch() reicht aus, um jede generierte Query oder Mutation typsicher auszufuehren, ganz ohne ein schwergewichtiges Client-Framework, sofern Persisted Queries und Caching nicht zwingend gebraucht werden.
// A thin, fully typed GraphQL client wrapper around fetch(), no framework required
import { print } from 'graphql';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
async function graphqlRequest<TResult, TVariables extends Record<string, unknown>>(
document: TypedDocumentNode<TResult, TVariables>,
variables: TVariables,
): Promise<TResult> {
const response = await fetch('https://shop.mironsoft.de/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Store: 'default' },
body: JSON.stringify({ query: print(document), variables }),
});
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
}
const payload = (await response.json()) as { data?: TResult; errors?: Array<{ message: string }> };
if (payload.errors?.length) {
throw new Error(payload.errors.map((error) => error.message).join(', '));
}
if (!payload.data) {
throw new Error('GraphQL response contained neither data nor errors');
}
return payload.data;
}
// Usage: variables and the return type are both inferred from the document
const result = await graphqlRequest(ProductsDocument, { search: 'jacket', pageSize: 12, currentPage: 1 });
result.products?.items?.[0]?.name; // string | null | undefined, exactly as generated
9. Untypisierter Fetch-Aufruf vs. codegen-typisierte Query im Vergleich
Der direkte Vergleich zwischen untypisierten Fetch-Aufrufen und codegen-typisierten Queries macht den praktischen Unterschied greifbar, gerade in Teams mit wechselnder Besetzung und regelmaessigen Magento-Upgrades.
| Aspekt | Untypisierter Fetch-Aufruf | Codegen-typisierte Query | Vorteil |
|---|---|---|---|
| Fehlererkennung | Erst zur Laufzeit im Browser | Compile-Fehler vor dem Deploy | Kaputte Felder vor Produktion abfangen |
| IDE-Autocomplete | Kein Wissen ueber Response-Form | Exakt aus generiertem Typ | Schnelleres, korrektes Query-Schreiben |
| Refactor-Sicherheit | Manuelle Suche nach Feldnamen | tsc findet jede betroffene Stelle | Sichere Schema-Migrationen |
| Umgang mit Nullability | Stillschweigend angenommen | Explizit im Typ sichtbar | Weniger undefined-Exceptions |
| Onboarding neuer Entwickler | Response-Form nur aus Doku/DevTools | Typen als lebendige Dokumentation | Schnellere Einarbeitung im Team |
Mironsoft
Headless-Magento-Integrationen mit typsicherer GraphQL-Anbindung
Typsichere GraphQL-Anbindung fuer euren Storefront?
Wir richten graphql-code-generator fuer euren Magento-Endpoint ein, typisieren Produkt-, Cart- und Checkout-Flows durchgaengig und verankern die Typpruefung fest in eurer CI-Pipeline, damit Schema-Aenderungen nie unbemerkt in Produktion landen.
Codegen-Setup
codegen.yml, Fragmente und TypedDocumentNode fuer Apollo oder urql einrichten
Cart & Checkout typisieren
Nullable-Handling, user_errors und Normalisierungsschicht sauber aufbauen
CI-Integration
Codegen-Diff-Check und tsc --noEmit gegen Schema-Drift in der Pipeline
10. Zusammenfassung
TypeScript und Magento GraphQL loesen gemeinsam ein Problem, das reine Fetch-Aufrufe strukturell nicht loesen koennen: Der GraphQL-Endpoint kennt seine eigenen Typen, der Client aber nicht, solange niemand diese Information explizit ins Projekt holt. graphql-code-generator schliesst genau diese Luecke, indem er Typen und typisierte Dokumentobjekte direkt aus dem echten Schema ableitet, inklusive aller Nullable-Angaben, die Magentos Schema bewusst grosszuegig vergibt. Produkt-Queries mit Fragmenten, Cart-Mutationen mit expliziter user_errors-Behandlung und eine Normalisierungsschicht fuer konsistente Domain-Typen bilden zusammen ein belastbares Fundament fuer Headless- und Hybrid-Storefronts.
Der Aufwand fuer Setup und CI-Integration zahlt sich vor allem dann aus, wenn mehrere Entwickler gleichzeitig am Storefront arbeiten und Magento regelmaessig aktualisiert wird, denn genau dann treten Schema-Drifts am haeufigsten auf. Wer Codegen als CI-Schritt verankert statt es nur lokal auszufuehren, verwandelt eine ganze Klasse stiller Laufzeitfehler in sichtbare, fruehzeitig behebbare Compile-Fehler und gewinnt damit Verlaesslichkeit, die sich direkt in weniger Produktionsvorfaellen niederschlaegt.
TypeScript und Magento GraphQL - Das Wichtigste auf einen Blick
Schema als Vertrag
Introspection oder exportierte schema.graphql als verlaessliche Quelle statt veralteter Dokumentation.
Codegen statt Handarbeit
graphql-code-generator erzeugt Typen und TypedDocumentNode-Objekte direkt aus Query und Schema.
Nullability ernst nehmen
Non-Null-Assertion, Optional Chaining oder Normalisierungsschicht bewusst je nach Risiko waehlen.
CI-Schutz gegen Drift
Codegen-Diff-Check und tsc --noEmit pro Pull Request verankern, Schema-Aenderungen frueh fangen.