Erkennung von stillen Abweichungen im API-Vertrag mithilfe von aus Beispielen abgeleiteten Typen und Zod
Warum handschriftlich erstellte TypeScript-Typen für Drittanbieter-APIs veralten, wie das Ableiten von Typen und Zod-Schemata aus tatsächlichen Antworten hilft, und wie Snapshot-Diffs Abweichungen aufzeigen.
Drittanbieter-APIs ändern sich ohne Vorwarnung, und TypeScript wird es nicht bemerken, denn Ihre Typen beschreiben, wie die Antwort aussah, als sie erstellt wurden, nicht wie sie heute aussieht. Dieser Artikel erläutert, wie dieses Versagen entsteht, warum das Erstellen von Typen aus mehreren tatsächlichen Antworten besser ist als ihr manuelles Eingeben, und warum der eigentliche Schutz darin besteht, neue Antworten mit einem gespeicherten Snapshot zu vergleichen. Sie werden außerdem erkennen, wo dieser Ansatz neben OpenAPI und Vertragsprüfung passt und wo nicht.
Wie ein umbenanntes Feld unbemerkt durchkommt
Betrachten Sie eine Frontend-Anwendung, die mit einem Zahlungsdienstleister integriert ist. Eines Tages ändert sich ein Feld in einer der Antworten des Anbieters von user_id auf userId. Es gibt weder einen Changelog-Eintrag noch eine Ankündigung oder eine Versionserhöhung. Wahrscheinlich hat ein Entwickler beim Anbieter einen inkonsistenten Namen korrigiert, der Testumfang bestanden und die Änderung wurde veröffentlicht.
Auf der Verbraucherseite stürzt nichts ab – und genau das ist das Problem. Der Code liest weiterhin response.user_id ein, und TypeScript akzeptiert das, weil die Schnittstelle Monate zuvor anhand eines Postman-Beispiels manuell typisiert wurde, das nicht mehr der Realität entspricht. Diese Schnittstelle verspricht weiterhin ein user_id-Feld. Bei der Ausführung ist der Wert einfach undefined. Zwei Wochen lang schreiben drei Codepfade stillschweigend undefined in ein Betragsfeld, bis schließlich ein Support-Ticket den Fehler aufdeckt. Es wird keine Warnung ausgelöst, keine Build wird rot angezeigt, und die Anwendung macht weiterhin das Falsche, ohne sich zu beschweren.
Teams, die längere Zeit mit externen APIs arbeiten, stoßen fast immer auf eine Variante dieses Problems.
Der Schwachpunkt liegt bei der Herkunft der Typen
TypeScript trägt hier keine Schuld. Das Problem liegt bei der Herkunft der Typen. Schnittstellen werden in der Regel so behandelt, als wären sie von einer autoritativen Quelle abgeleitet, wie beispielsweise einem Schema, einem Vertrag oder einer einzigen Wahrheitsquelle. In der Praxis stammen viele davon aus einer Beispielantwort, die jemand von Hand transkribiert hat. Diese Schnittstelle wird anschließend in mehrere andere Dateien kopiert und als Fakt angesehen, ohne dass jemand sie erneut prüft, bis etwas schiefgeht.
Der eigentliche Vertrag ist das, was die API derzeit in der Produktion zurückgibt. Er befindet sich auf einem Server, den Sie nicht kontrollieren können, und kann ohne Ihre Zustimmung geändert werden. Handgeschriebene Typen sind ein Zeitpunktfoto eines bereits vergangenen Moments, und der Compiler hat keine Möglichkeit, das zu wissen.
Es gibt außerdem eine tiefere Lücke. TypeScript-Typen verschwinden zur Kompilierzeit, wodurch während der Laufzeit nie etwas überprüft wird. Wenn sich die Struktur des Payloads ändert, führt nur eine Laufzeitvalidierung an der Grenze – beispielsweise die Verarbeitung der Antwort mit einem Zod-Schema – dazu, dass ein stilles undefined in einen sofort sichtbaren Fehler umgewandelt wird.
Typen aus mehreren tatsächlichen Antworten ableiten
Was hier am meisten hilft, sind weder fortgeschritteneres TypeScript noch klügere Generics, sondern ein mechanischer Prozess: Man nimmt die tatsächlich von der API zurückgegebenen Antworten, generiert daraus Typen und erhält eine Meldung, sobald die Realität nicht mehr übereinstimmt.
Die Eingabe sollten echte JSON-Antworten sein, keine Dokumentation und kein Schema. Aus diesen kann ein Tool sowohl einen TypeScript-Typ als auch ein entsprechendes Zod-Schema ableiten. Die Verwendung mehrerer Beispiele ist wichtiger, als es auf den ersten Blick scheint. Eine Antwort zeigt, wie eine Payload aussehen kann. Drei oder vier Antworten machen deutlich, welche Felder tatsächlich optional sind, welche manchmal null haben und welche Array-Elemente uneinheitliche Strukturen aufweisen. Ein einzelnes Beispiel irreführt stets durch Auslassungen.
In dem untenstehenden Beispiel werden zwei Antworten für dieselbe Ressource eingegeben – eine mit user_id und eine mit userId – und es werden der aus beiden abgeleitete TypeScript-Typ sowie das Zod-Schema gezeigt:
// paste these two responses in...
[
{
"user_id": "pot_00009exampleP0tOxWb",
"name": "Wedding Fund",
"balance": 550100,
"currency": "GBP",
"created": "2025-11-09T12:30:53.695Z",
"updated": "2025-02-26T07:12:04.925Z"
},
{
"userId": "pot_00009exampleP0tOxWb",
"name": "Wedding Fund",
"balance": 550,
"currency": "EUR",
"created": "2025-11-09T12:30:53.695Z",
"updated": "2025-03-26T07:12:04.925Z"
}
]
// ...get this out typescript
type Root = {
user_id?: string
name: string
balance: number
currency: string
created: string
updated: string
userId?: string
}[]
// or ... get this out zod
import { z } from 'zod'
const Root = z.array(z.object({
user_id: z.string().optional(),
name: z.string(),
balance: z.number(),
currency: z.string(),
created: z.string(),
updated: z.string(),
userId: z.string().optional(),
}))
Betrachten Sie genau, was das zusammengeführte Ergebnis anzeigt. Da jeder Name nur in einem Beispiel vorkommt, werden sowohl user_id als auch userId zu optionalen Feldern. Das ist technisch korrekt, verdeckt aber gleichzeitig die Umbenennung: Code, der eines dieser Felder liest, führt weiterhin eine Typüberprüfung durch, und eine Antwort, die keines dieser Felder enthält, würde ebenfalls das Zod-Schema bestehen. Die Beispiele deuten zudem auf ein Problem hin, das durch Typinferenz niemals erkannt werden kann: balance sinkt von 550100 auf 550, während sich currency ändert – was auf einen Wechsel zwischen kleineren und größeren Währungseinheiten hindeuten könnte. In beiden Fällen wird der inferte Typ number angegeben. Die Inferenz gibt Ihnen die Struktur an, kann Ihnen aber nicht die Bedeutung mitteilen.
Viele Code-Generatoren stoppen an diesem Punkt. Vom untypisierten zum typisierten Zustand zu gelangen ist nützlich, löst aber das Problem der Abweichungen nicht.
Snapshots und Diffs erkennen die Änderung
Der wertvollere Schritt erfolgt nach der Erstellung. Sobald Typen aus einer tatsächlichen Antwort abgeleitet wurden, kann diese Antwort als Snapshot gespeichert werden. Jedes Mal, wenn Sie eine neue Probe vom selben Endpunkt abrufen, vergleichen Sie sie mit dem Snapshot und erhalten so einen genauen Bericht darüber, was sich geändert hat – ob es sich um ein Feld mit neuem Namen handelt, um einen Wert, der nun optional ist, wo er früher ein einfacher String war, oder um einen zusätzlichen Schlüssel in einem verschachtelten Objekt. Anstelle der vagen Angabe „Irgendwo ist etwas schiefgelaufen“ sehen Sie den genauen Unterschied in der Struktur.
Diese Vergleichsweise ist es, die einen Typgenerator von einem Drift-Detector unterscheidet. Die Codegenerierung ermöglicht es, vom Nichts aus typisierten Code zu erstellen. Der Drift-Detector sorgt dafür, dass eine Umbenennung von user_id in userId nicht wochenlang unbemerkt in der Produktion bleibt. Im obigen Beispiel würde ein Snapshot-Diff „user_id entfernt, userId hinzugefügt“ melden, anstatt beide Felder stillschweigend zu optionalen Feldern zu machen.
Lassen Sie Produktionsdatenpakete auf Ihrem Rechner
Um einen echten Drift zu erkennen, sind echte Daten erforderlich – synthetische Payloads zeigen nicht die Veränderungen, die für Sie relevant sind. Deshalb ist Datenschutz eine Anforderung bei der Systemgestaltung. Produktionsantworten können Kundendaten enthalten, wodurch das Einfügen dieser Daten in ein Webformular, das sie auf einen Server eines Dritten hochlädt, ein neues Risiko bei der Datenverarbeitung schafft. Tools für diese Aufgabe sollten lokal ausgeführt werden, beispielsweise vollständig in einem Browserfenster oder als Skript in Ihrem eigenen Repository, damit die Payloads niemals Ihr Umfeld verlassen.
Wo dieser Ansatz passt und wo nicht
Infereenz auf Basis von Beispielen mit Drifterkennung ersetzt weder OpenAPI noch eine Kontrakt-Testumgebung wie Pact. Wenn Sie sowohl den Anbieter als auch den Verbraucher kontrollieren und ein Schema am Quellcode durchsetzen können, sollten Sie dies tun – das ist die bessere langfristige Lösung.
Diese Technik richtet sich auf die häufigere und weniger attraktive Situation: Man nutzt eine API, die man nicht kontrolliert, deren Dokumentation veraltet oder fehlt, und es ist nicht möglich, einen Client aus einer OpenAPI-Spezifikation zu erstellen – entweder weil es keine Spezifikation gibt oder niemand ihr vertraut. Das beschreibt die meisten Integrationen mit Zahlungsprozessoren, internen Diensten anderer Teams sowie APIs von Drittanbietern. In dieser Situation ist die tatsächliche Antwort die einzige verfügbare Referenz, von der Ihre Typen abgeleitet werden sollten.
Bleiben Sie auf einem engen Rahmen. JSON einbeziehen, TypeScript und Zod ausschließen – zusammen mit einer Drift-Erkennung reicht das aus. Der Versuch, XML, protobuf sowie alle möglichen Randfälle von Schemata zu handhaben, verwandelt ein präzises Werkzeug in eines Unklaren. Für einen umfassenderen Überblick über die Vertragsentscheidungen, die Frontends schaden können, siehe gängige Fehler bei API-Verträgen, die die Zuverlässigkeit von Frontends beeinträchtigen.
Ein praktischer Ersttest
Der beste Ort, um dies auszuprobieren, ist eine Integration, die bereits unter einer stillen Strukturänderung gelitten hat. Nehmen Sie eine alte Antwort sowie eine aktuelle von derselben Endpunkt, führen Sie sie durch die Inferenz- und Vergleichsfunktionen und prüfen Sie, was als Problem markiert wird. Der Anblick einer tatsächlichen historischen Änderung im Diff ist überzeugender als jedes Argument für diesen Ansatz.
Kernpunkte
- Handgeschriebene Schnittstellen für Drittanbieter-APIs sind Zeitfotos der Vergangenheit, und TypeScript kann nicht erkennen, wann sie veraltet werden.
- Erfassen Sie Typen sowie Zod-Schemata aus mehreren tatsächlichen Antworten, da mehrere Beispiele optionale, fehlertolerante sowie inkonsistente Felder aufzeigen, die in einem einzigen Beispiel verborgen bleiben.
- Überprüfen Sie die Antworten laufend am API-Grenzpunkt, damit Formänderungen deutlich sichtbar werden anstatt
undefinedzu erzeugen. - Speichern Sie die Antworten als Zeitfotos und vergleichen Sie neue Beispiele mit ihnen; allein die Kombination aus Inferenz kann eine Umbenennung als zwei optionale Felder verschleiern.
- Lassen Sie Produktionsdaten lokal und bevorzugen Sie OpenAPI oder Vertragstests, wenn Sie beide Seiten der API kontrollieren können.
Verwandte Literatur
- API-Vertragsabweichungen zur Laufzeit durch eine schrittweise tRPC-Einführung erkennen — Wie tRPC ein umbenanntes Backend-Feld in einen Kompilierfehler umwandelt, wie man es neben REST Endpoint für Endpoint einführt und wo es das falsche Werkzeug ist.
- Sechs TypeScript-Techniken, die Typen in echte Fehlerverhinderungsmittel verwandeln — Erfahren Sie, wie satisfies, markierte Unionen, never checks, unknown, abgeleitete Typen und branded IDs dazu beitragen, dass TypeScript echte Fehler zur Laufzeit statt in der Produktion erkennt.