Wie Frontend und Backend garantiert synchron bleiben
Handgeschriebene TypeScript-Interfaces für REST-Endpunkte veralten schleichend, sobald sich das Backend weiterentwickelt, und liefern dann trügerische Typsicherheit statt echtem Schutz vor Laufzeitfehlern. Dieser Artikel zeigt, wie openapi-typescript und verwandte Tools TypeScript-Typen sowie typisierte Fetch-Clients direkt aus einer OpenAPI-Spezifikation erzeugen und wie sich dieser Prozess zuverlässig in Build- und CI-Pipelines verankern lässt.
Inhaltsverzeichnis
- 1. Das Drift-Problem handgeschriebener API-Typen
- 2. Was eine OpenAPI-Spezifikation ist und warum sie als Single Source of Truth taugt
- 3. openapi-typescript: reine Typen ohne Laufzeit-Overhead
- 4. Einen typisierten Fetch-Client auf den generierten Typen aufbauen
- 5. Versionierte APIs und mehrere Specs in einem Projekt verwalten
- 6. Codegen in npm-Skripte und CI verankern
- 7. Breaking-Change-Erkennung zwischen alten und neuen Typen
- 8. Grenzen und Fallstricke: unvollständige Specs und Nullable-Ambiguitäten
- 9. Handgeschriebene vs. generierte API-Typen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Drift-Problem handgeschriebener API-Typen
Das klassische Muster in wachsenden Magento- und Headless-Projekten: Ein Entwickler schreibt zu Projektbeginn ein interface Product { id: number; name: string; price: number; }, weil es genau der aktuellen API-Antwort entspricht. Monate später fügt das Backend-Team ein neues Pflichtfeld hinzu, macht price optional für Draft-Produkte oder benennt ein Feld um. Der handgeschriebene Typ bleibt unverändert, TypeScript kompiliert weiterhin fehlerfrei, weil der Compiler die reale API-Antwort gar nicht kennt - er prüft nur gegen die Behauptung im Interface, nicht gegen die Wahrheit auf dem Server.
Das Ergebnis ist trügerische Typsicherheit: Der Code sieht abgesichert aus, bricht aber zur Laufzeit mit undefined is not an object, sobald ein Feld fehlt, das der Typ als garantiert vorhanden deklariert. Code-Reviews erkennen diese Drift selten, weil niemand bei jeder PR die tatsächliche API-Antwort mit dem Interface abgleicht. Mit der Zeit sinkt das Vertrauen ins Typsystem, Teams beginnen any als Fluchtventil einzusetzen, und genau der Nutzen, für den TypeScript ursprünglich eingeführt wurde, geht schleichend verloren.
2. Was eine OpenAPI-Spezifikation ist und warum sie als Single Source of Truth taugt
OpenAPI (früher Swagger) ist ein maschinenlesbares YAML- oder JSON-Format zur Beschreibung von REST-APIs: Pfade, HTTP-Methoden, Parameter, Request- und Response-Schemas sowie Authentifizierung werden vollständig deklarativ erfasst. Viele Backend-Frameworks generieren diese Spec automatisch aus Code-Annotationen statt sie manuell zu pflegen - Symfony API Platform, NestJS mit @nestjs/swagger, Laravel mit L5-Swagger, und auch Magentos eigene REST-API liefert eine vollständige Swagger-Definition unter /rest/V1/schema beziehungsweise /swagger aus.
Wird die Spec direkt aus dem Backend-Code generiert, kann sie strukturell nicht veralten: Jede Änderung an einem Controller oder DTO spiegelt sich automatisch in der nächsten Spec-Generierung wider. Damit wird die OpenAPI-Datei zum verbindlichen Vertrag zwischen Backend- und Frontend-Team, und genau dieser Vertrag lässt sich als Eingabe für die TypeScript-Typgenerierung nutzen, statt Typen ein zweites Mal von Hand nach der Dokumentation abzutippen.
openapi: 3.1.0
info:
title: Mironsoft Product API
version: "1.2.0"
paths:
/products/{sku}:
get:
operationId: getProductBySku
parameters:
- name: sku
in: path
required: true
schema:
type: string
responses:
"200":
description: A single product
content:
application/json:
schema:
$ref: "#/components/schemas/Product"
"404":
description: Product not found
components:
schemas:
Product:
type: object
required:
- id
- sku
- name
- price
properties:
id:
type: integer
sku:
type: string
name:
type: string
price:
type: number
format: float
specialPrice:
type: number
format: float
nullable: true
tags:
type: array
items:
type: string
3. openapi-typescript: reine Typen ohne Laufzeit-Overhead
openapi-typescript liest eine OpenAPI-Datei, egal ob als lokale YAML/JSON-Datei oder per URL von einer laufenden Instanz abgerufen, und erzeugt daraus eine einzige .d.ts-Datei mit einem paths-Interface für alle Endpunkte sowie einem components["schemas"]-Interface für alle Datenmodelle. Entscheidend ist, dass das Tool ausschließlich Typen erzeugt, keine Laufzeit-Bibliothek: Es entstehen keine zusätzlichen Bytes im Produktions-Bundle, weil Typinformationen beim Kompilieren restlos entfernt werden. Das unterscheidet es von schwergewichtigeren Codegen-Ansätzen, die vollständige Client-Klassen mit eigener Laufzeitlogik erzeugen.
In der Praxis reicht ein einziger Befehl: npx openapi-typescript ./openapi/product-api.yaml -o ./src/api/schema.d.ts. Ein Watch-Modus regeneriert die Typen automatisch bei jeder Änderung der lokalen Spec-Datei, was sich besonders während der gemeinsamen Backend-Frontend-Entwicklung auszahlt. Wird die Spec stattdessen live von einer laufenden Magento-Instanz per URL gezogen, spiegeln die generierten Typen exakt den Zustand der tatsächlich erreichbaren API wider, nicht nur eine irgendwann eingecheckte Momentaufnahme.
// Auto-generated by openapi-typescript. DO NOT EDIT BY HAND.
export interface paths {
"/products/{sku}": {
get: operations["getProductBySku"];
};
}
export interface components {
schemas: {
Product: {
id: number;
sku: string;
name: string;
price: number;
/** @description Nullable in the spec, so number | null in TS */
specialPrice: number | null;
tags?: string[];
};
};
}
export interface operations {
getProductBySku: {
parameters: {
path: { sku: string };
};
responses: {
200: {
content: { "application/json": components["schemas"]["Product"] };
};
404: {
content: never;
};
};
};
}
4. Einen typisierten Fetch-Client auf den generierten Typen aufbauen
Reine Typen allein verhindern noch keine Laufzeitfehler - erst ein Client, der diese Typen tatsächlich durchsetzt, schließt den Kreis. openapi-fetch, aus derselben Werkzeugfamilie wie openapi-typescript, ist ein wenige Kilobyte großer Wrapper um die native fetch-API, der Pfade, HTTP-Methode, Parameter und Response-Body vollständig gegen das generierte paths-Interface prüft. Die IDE bietet dabei Autovervollständigung für gültige Pfade und Parameter, und ein Tippfehler im Endpunkt-Namen wird zum Compile-Fehler statt zu einem stillen 404 zur Laufzeit.
Wer keine zusätzliche Abhängigkeit im Bundle haben möchte, kann denselben Effekt mit einem schlanken, selbst geschriebenen Generic-Wrapper um fetch erreichen, der die generierten operations-Typen als Typparameter nutzt. Wichtig bleibt in beiden Fällen: Die Typprüfung findet ausschließlich zur Compile-Zeit statt. Ob die Response zur Laufzeit tatsächlich dem Schema entspricht, prüft weder openapi-typescript noch openapi-fetch - dafür braucht es bei Bedarf eine zusätzliche Laufzeitvalidierung, etwa mit zod.
import createClient from "openapi-fetch";
import type { paths } from "./api/schema";
// The client is fully typed against the generated `paths` interface
const client = createClient<paths>({ baseUrl: "https://shop.example.com" });
async function loadProduct(sku: string) {
const { data, error } = await client.GET("/products/{sku}", {
params: { path: { sku } },
});
if (error) {
// error is typed based on the non-2xx responses in the spec
console.error("Product lookup failed", error);
return null;
}
// data.price is a number, data.specialPrice is number | null -
// both checked at compile time against the OpenAPI schema
return data;
}
5. Versionierte APIs und mehrere Specs in einem Projekt verwalten
Reale Magento-Projekte sprechen selten nur mit einer einzigen API. Typisch ist eine Kombination aus der Magento REST API für Katalog, Warenkorb und Kunde, plus einem oder mehreren internen Microservices, etwa für eine eigene Preisengine oder eine externe Suche. Jede dieser Quellen bekommt eine eigene generierte Typdatei mit eindeutigem Namen, zum Beispiel schema-magento.d.ts und schema-pricing.d.ts, damit gleichnamige Schema-Typen wie Product aus unterschiedlichen Services nicht kollidieren und über getrennte Namespace-Importe eindeutig referenzierbar bleiben.
Für Versionierung gilt ein ähnliches Prinzip: Führt das Backend eine Breaking-Change-Version v2 ein, wird zunächst eine zweite Spec parallel zur bestehenden v1-Spec generiert, sodass beide Typversionen koexistieren, bis die Migration im Frontend abgeschlossen ist. Klar benannte npm-Skripte pro Spec, etwa generate:magento und generate:pricing, halten diesen Prozess nachvollziehbar und verhindern, dass ein einzelner globaler Codegen-Befehl unübersichtlich wird, sobald ein Projekt über drei oder vier Backends hinweg wächst.
6. Codegen in npm-Skripte und CI verankern
Ohne Automatisierung verkommt Typgenerierung zu einem manuellen Schritt, den irgendwann jemand vergisst. Die einfachste Absicherung sind prebuild- und predev-Hooks in package.json, die den Codegen-Befehl automatisch vor jedem Build beziehungsweise Entwicklungsstart ausführen. So können lokal entwickelte Typen nie länger als eine Session veralten, weil sie bei jedem Start neu aus der aktuellen Spec gezogen werden.
In der CI-Pipeline kommt ein zweiter, härterer Schritt hinzu: die Spec wird frisch vom Backend abgerufen, die Typen werden neu generiert, und anschließend prüft git diff --exit-code die generierten Dateien gegen den committeten Stand im Repository. Weicht das Ergebnis ab, schlägt der Build fehl - ein klares Signal, dass sich das Backend-Schema geändert hat, ohne dass die Frontend-Typen regeneriert und committet wurden. Dieser Schema-Drift-Check macht aus einem stillen, schleichenden Problem einen lauten, sofort sichtbaren CI-Fehler.
{
"scripts": {
"generate:api-types": "openapi-typescript ./openapi/product-api.yaml -o ./src/api/schema.d.ts",
"generate:pricing-types": "openapi-typescript ./openapi/pricing-api.yaml -o ./src/api/pricing-schema.d.ts",
"prebuild": "npm run generate:api-types && npm run generate:pricing-types",
"predev": "npm run generate:api-types && npm run generate:pricing-types",
"build": "vite build"
}
}
- name: Regenerate API types and check for drift
run: |
npm run generate:api-types
npm run generate:pricing-types
# Fail the build if the freshly generated types differ from
# what is committed in the repository - this is schema drift.
git diff --exit-code -- src/api/schema.d.ts src/api/pricing-schema.d.ts
- name: Fail with a clear message on drift
if: failure()
run: |
echo "Generated API types are out of sync with the OpenAPI spec."
echo "Run 'npm run generate:api-types' locally and commit the result."
exit 1
7. Breaking-Change-Erkennung zwischen alten und neuen Typen
Weil die generierte .d.ts-Datei deterministisch und für Menschen lesbar ist, wird ein einfacher git diff auf diese Datei zu einem überraschend wirksamen Werkzeug: Ein entferntes Property, ein von string zu string | null geändertes Feld oder ein neues Pflichtfeld tauchen im Pull-Request-Diff exakt dort auf, wo sie hingehören - sichtbar, kommentierbar, reviewbar. Bei handgeschriebenen Typen passiert dieselbe Änderung oft still, weil ein Entwickler den Typ "nebenbei" an die neue Realität anpasst, ohne dass der eigentliche Auslöser im Backend jemals im Diff sichtbar wird.
Für noch präzisere Aussagen lassen sich dedizierte Diff-Tools wie oasdiff oder openapi-diff einsetzen, die zwei Spec-Versionen semantisch vergleichen und Änderungen automatisch als breaking oder non-breaking klassifizieren - ein neues optionales Feld gilt als sicher, ein entferntes oder neu verpflichtendes Feld als Breaking Change. Die Kombination aus generiertem Typ-Diff und semantischem Spec-Diff gibt in der CI-Pipeline eine doppelte Absicherung: Das eine zeigt die konkrete TypeScript-Auswirkung, das andere die fachliche Schwere der Änderung.
8. Grenzen und Fallstricke: unvollständige Specs und Nullable-Ambiguitäten
Codegen ist nur so vertrauenswürdig wie die zugrunde liegende Spec selbst. Wird eine Swagger-Datei von Hand gepflegt statt aus dem Code generiert, driftet sie mit der Zeit genauso wie handgeschriebene Typen, nur eine Ebene höher im Stack. Fehlt in der Spec ein korrektes required-Array, markiert openapi-typescript fast alle Felder als optional, was TypeScript zwingt, überall Optional-Chaining zu verwenden, selbst dort, wo ein Feld in der Praxis immer vorhanden ist - ein Vorteil an Sicherheit erkauft mit unnötig sperrigem Code.
Auch nullable-Semantik ist zwischen Frameworks nicht einheitlich: Manche Backends geben ein fehlendes Feld zurück, andere null, wieder andere einen Platzhalter wie "" oder 0, und all das kann unterschiedlich in die Spec übersetzt werden. Erschwerend kommt hinzu, dass OpenAPI 3.0 und 3.1 nullable syntaktisch verschieden behandeln: 3.0 nutzt nullable: true, 3.1 folgt dem JSON-Schema-Standard mit type: ["string", "null"]. Ein sauberer Codegen-Workflow ersetzt daher nicht das Gespräch mit dem Backend-Team über ein gemeinsames Verständnis von Pflichtfeldern.
9. Handgeschriebene vs. generierte API-Typen im Vergleich
Die folgende Tabelle fasst zusammen, worin sich handgeschriebene und aus OpenAPI generierte TypeScript-Typen in der Praxis unterscheiden - über die reine Anfangsersparnis hinaus, die beim ersten Anlegen eines Interfaces oft täuschend gering wirkt.
| Kriterium | Handgeschriebene Typen | Generierte Typen |
|---|---|---|
| Genauigkeit über Zeit | Driftet unbemerkt bei jeder Backend-Änderung | Immer synchron mit der aktuellen Spec |
| Pflegeaufwand | Manuell pro Endpunkt, skaliert schlecht | Ein Befehl für alle Endpunkte gleichzeitig |
| Onboarding-Geschwindigkeit | Neue Entwickler lesen Docs und raten Felder | IDE-Autovervollständigung direkt aus der Spec |
| Breaking-Change-Erkennung | Fällt erst zur Laufzeit auf | Sichtbar im Diff, blockierbar in CI |
| Nullable-Handling | Abhängig von der Disziplin des Entwicklers | Direkt aus dem Schema abgeleitet, konsistent |
Der Vergleich zeigt kein Nischenwerkzeug für Großprojekte, sondern einen strukturellen Vorteil, der sich bereits ab wenigen Dutzend Endpunkten auszahlt. Generierte Typen verschieben Fehler konsequent von der Laufzeit in die Compile-Zeit oder sogar in die CI-Pipeline, lange bevor Code überhaupt in Produktion läuft.
Mironsoft
TypeScript-Codegen und typsichere API-Integrationen für Magento- und Headless-Projekte
API-Typen zuverlässig synchron halten?
Wir richten openapi-typescript und typisierte Fetch-Clients für eure Magento REST API und interne Microservices ein und verankern Schema-Drift-Checks fest in eurer CI-Pipeline, damit veraltete API-Typen erst gar nicht mehr entstehen.
OpenAPI-Codegen-Setup
Typgenerierung aus Magento- und Microservice-Specs einrichten
CI-Pipeline-Integration
Schema-Drift-Checks und Breaking-Change-Diffs in GitHub Actions
Typisierte API-Clients
openapi-fetch-Wrapper mit optionaler Laufzeitvalidierung via zod
10. Zusammenfassung
API-Typen automatisch aus OpenAPI/Swagger zu generieren löst ein Problem, das handgeschriebene Interfaces strukturell nicht lösen können: garantierte Übereinstimmung zwischen dem, was der Typ verspricht, und dem, was das Backend tatsächlich liefert. openapi-typescript erzeugt reine, laufzeitfreie Typen direkt aus der Spec, openapi-fetch oder ein schlanker eigener Wrapper macht diese Typen an jedem API-Aufruf tatsächlich wirksam, und Schema-Drift-Checks in der CI-Pipeline verhindern, dass veraltete Typen überhaupt gemergt werden können.
Der entscheidende Hebel liegt nicht im einzelnen Codegen-Befehl, sondern in der Automatisierung: prebuild-Hooks für die lokale Entwicklung und ein harter git diff --exit-code-Schritt in CI sorgen dafür, dass Frontend-Typen niemals mehr als einen Build-Zyklus hinter dem echten API-Vertrag zurückfallen. Bleiben die Grenzen des Ansatzes im Blick, allen voran unvollständige oder handgepflegte Specs und uneinheitliche Nullable-Semantik, wird aus Typgenerierung ein verlässlicher, fast unsichtbarer Teil der täglichen Entwicklungsroutine.
API-Typen aus OpenAPI generieren - Das Wichtigste auf einen Blick
Single Source of Truth
Die OpenAPI-Spec, idealerweise aus Backend-Code generiert, ersetzt handgeschriebene Interfaces als verbindlichen Vertrag.
Zero-Runtime-Typen
openapi-typescript erzeugt reine .d.ts-Dateien ohne zusätzliches Bundle-Gewicht in Produktion.
CI Drift-Check
git diff --exit-code auf frisch generierte Typen macht Schema-Drift zu einem sofort sichtbaren Build-Fehler.
Breaking-Change-Diff
Typ-Diffs und semantische Spec-Diffs wie oasdiff zeigen Änderungen, bevor sie das Frontend brechen.