Warum wir keine SDKs mehr verwenden

@alvinsng
ENGLISCHvor 1 Tag · 14. Juli 2026
435K
674
52
55
1.0K

TL;DR

Alvin Sng erklärt, warum sein Team Vendor-SDKs durch einen benutzerdefinierten HTTP-Client ersetzt hat, um das Debugging zu verbessern, die Observability zu vereinheitlichen und unnötigen Dependency-Ballast zu eliminieren.

Wir haben die Nutzung der Client-SDKs von Stripe, WorkOS und Slack eingestellt und migrieren die restlichen gerade ebenfalls weg. Stattdessen rufen wir deren REST-APIs direkt über eine kleine Wrapper-Klasse namens HttpBaseClient auf.

Das klingt rückwärtsgewandt. SDKs sollen doch Zeit sparen. Aber du hörst ja ständig, dass KI mehr Code nativ ausführen lässt. Der gleiche Gedanke gilt für SDKs. Warum?

  • SDKs verbergen die rohen Details wie HTTP-Response-Header und rohe Response-Bodies – genau das, was Agenten brauchen, um Probleme zu debuggen oder Details an das upstream Team weiterzugeben.
  • Sie erwarten wohlgeformte Antworten, aber in der Realität kommen fehlerhafte Antworten von Firewalls, Load-Balancern und Gateways zurück. Das verdeckt die Debugging-Details, die wir brauchen.
  • SDKs umgehen den Zwang, einen zentralen Einstiegspunkt für Retries, Fehlerbehandlung und Observability zu haben – und erlauben es Agenten, eigene Muster zu schreiben.
  • Sie sind aufgebläht, meist automatisch aus OpenAPI generiert, obwohl wir nur einen winzigen Teil der Endpunkte nutzen.

Überleg mal, warum SDKs überhaupt entstanden sind. Sie wurden geboren, weil Unternehmen die Adoption einfach machen wollten: eine gemeinsame Bibliothek ausliefern, Kunden Zeit sparen und die lästige Integrationsarbeit verstecken. Aber KI hat diese Kostenkurve verschoben. Die Integration mit einem SDK ist heute oft genauso aufwändig wie der direkte Aufruf der HTTP-API, und es ist billig, einen maßgeschneiderten REST-Client genau für die Endpunkte zu schreiben, die wir nutzen – und dabei gleichzeitig eine bessere, einheitliche Observability zu bieten.

Das endlose Spiel der Whack-a-Mole

Alvin Sng - inline image

So schöne „JSON"-Antworten

So sah unser Sentry-Fehler-Dashboard früher aus. Wir stellten fest, dass ein kleiner Prozentsatz der Anfragen über einen bestimmten Codepfad fehlschlug, dann patchten wir genau diesen Pfad, um den SDK-Fehler für den Benutzer abzufangen. Am nächsten Tag stolperte ein anderer Codepfad über einen unerwarteten Fehler – wir patchten auch den, und das Spiel endete nie.

SDKs sind anfällig für einen häufigen Produktionsfehlermodus: Der Server meldet, dass etwas schiefgelaufen ist, aber die Antwort hat nicht die JSON-Form, die das SDK erwartet.

Ein überlasteter Nginx-Gateway gibt unerwartetes HTML zurück. Cloudflare blockiert eine Anfrage. Eine Firewall zeigt eine Rate-Limit-Seite an. Die Vendor-API ist normalerweise JSON, aber das davor ist nicht immer die Vendor-API.

Wenn das passiert, versuchen viele SDKs, die Antwort zu parsen, scheitern mit einem generischen Parsing-Fehler und werfen die nützlichen Teile weg. Der rohe Body ist weg. Der Status-Text ist vergraben. Die HTTP-Header werden oft nicht nutzbar zurückgegeben. Wenn der Vendor-Support nach einer Request-ID aus einem HTTP-Header fragt, haben wir Pech gehabt – weil das SDK sie nicht zurückgibt.

Agenten missbrauchen die Direktheit von SDKs

Unsere Client-SDK-Aufrufe waren der Wilde Westen. Stripe hatte ein Muster. WorkOS ein anderes. Slack hatte seine eigenen Eigenheiten. Andere Integrationen hatten rohes Fetch, SDK-Aufrufe, einmalige Retry-Logik oder gar keine Retry-Logik.

SDKs machten das Falsche einfach. Ein Agent konnte jederzeit direkt zum Vendor-Client gehen und \stripe.customers.create(...)\ in einer beliebigen Route aufrufen. Das fühlt sich produktiv an, umgeht aber den gemeinsamen Ort, an dem Auth, Retries, Metriken, Logs und Fehlerübersetzung leben sollten. Wir hatten solche Closure-Wrapper überall in unserer Codebasis verstreut:

typescript
1const response = await catchRateLimitError(() =>
2 stripe.customers.retrieve(stripeCustomerId)
3);

Wenn du an einer Stelle vergessen hast, den SDK-Aufruf zu wrappen, hattest du einen schlechten Tag. Ein Stripe-Rate-Limit und ein WorkOS-Rate-Limit bedeuten für unser Produkt dasselbe: Der Upstream bittet uns, langsamer zu machen. Aber auf Typebene waren das völlig unterschiedliche Objekte. Manche Code-Teile fingen SDK-spezifische Exceptions ab. Manche fingen generische Errors. Manche fingen gar nichts. Das führte zu Sentry-Whack-a-Mole: Einen 429 an einer Aufrufstelle fixen, auf die gleiche Fehlerklasse an einer anderen Stelle warten.

SDKs sind aufgebläht

Die NPM-Pakete openai und anthropic-ai/sdk werden von Stainless automatisch aus OpenAPI-Spezifikationen generiert. Stripe wird ebenfalls aus Stripe's OpenAPI-Spezifikation generiert. So skalierst du die Wartung eines öffentlichen SDKs für eine große API in Dutzenden Programmiersprachen.

Aber ein großartiges öffentliches SDK muss jedem gerecht werden. Unser Backend braucht nicht jedermanns SDK. Es braucht unsere acht Stripe-Endpunkte, unsere WorkOS-Benutzer- und Organisations-Endpunkte und die Slack-Methoden, die wir tatsächlich aufrufen. Stripe ist 6,5 MB, workos-inc/node ist 6,9 MB, slack/web-api ist 7,7 MB und linear/sdk ist 34 MB. Im äußersten Extremfall erreicht googleapis 198 MB.

Generierte SDKs bringen die gesamte Plattform mit: Hunderte Methoden, Überladungen, Paginierungs-Helfer, Retry-Verhalten, Umgebungserkennung, Kompatibilitäts-Shims und alte Oberflächen, die nicht verschwinden können, weil irgendwo jemand davon abhängt. In unserem eigenen Backend ist diese Allgemeinheit meist Bloat. Schlimmer noch: Sie steht zwischen uns und der Leitung.

Wir vergessen, dass HTTP-APIs API-Verträge sind

Manche Leute sprechen von HTTP-APIs, als wären sie Implementierungsdetails auf niedrigerer Ebene und SDKs die eigentliche stabile Schnittstelle. Die öffentliche REST-API ist ein Vertrag. Anbieter können sie nicht einfach so brechen. SDK-Autoren wissen das auch, denn sie können nicht jeden Kunden zum Upgrade zwingen. Viele Kunden betreiben jahrealte SDK-Versionen in Produktion, was bedeutet, dass der alte Wire-Vertrag trotzdem funktionieren muss.

Unser eigenes HttpBaseClient

HttpBaseClient ist unser Ersatz für Client-SDKs. Provider-Subklassen liefern die anbieterspezifischen Teile: Basis-URL, Auth-Header, Content-Type, Fehlerzuordnung und schmale Methoden für die Endpunkte, die wir tatsächlich nutzen. HttpBaseClient übernimmt den Rest: Serialisierung, Parsing, Transportfehler, strukturierte Logs, Metriken, Statuszuordnung und Dauer-Tracking. Das vereinheitlicht die Observability, sodass jeder Anbieter einheitliche Standards einhält. Hier ist die vereinfachte Struktur:

typescript
1abstract class HttpBaseClient<TEndpoint extends string> {
2 protected abstract readonly baseUrl: string;
3
4 protected constructor(private readonly dependency: string) {}
5
6 protected abstract buildAuthHeaders(): Promise<Record<string, string>>;
7
8 protected async request<TBody, TResponse>(config: {
9 method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
10 path: string;
11 endpoint: TEndpoint;
12 body?: TBody;
13 }): Promise<TResponse> {
14 const url = `${this.baseUrl}${config.path}`;
15 const headers = await this.buildAuthHeaders();
16 const labels = {
17 dependency: this.dependency,
18 endpoint: config.endpoint,
19 method: config.method,
20 };
21 const start = performance.now();
22
23 logInfo('upstream request starting', labels);
24
25 try {
26 const response = await callWithMetrics(
27 () =>
28 fetch(url, {
29 method: config.method,
30 headers,
31 body: config.body === undefined ? undefined : JSON.stringify(config.body),
32 }),
33 this.dependency,
34 labels
35 );
36
37 const body = await parseBody(response);
38 if (!response.ok) throw this.mapHttpError(response, body);
39
40 logInfo('upstream request succeeded', {
41 ...labels,
42 statusCode: response.status,
43 durationMs: performance.now() - start,
44 });
45
46 return body as TResponse;
47 } catch (cause) {
48 logWarn('upstream request failed', {
49 ...labels,
50 durationMs: performance.now() - start,
51 cause,
52 });
53 throw cause;
54 }
55 }
56}
57
58// Then a Stripe wrapper becomes small and explicit:
59enum StripeEndpoint {
60 CustomersCreate = 'stripe/customers/create',
61}
62
63class StripeHttpClient extends HttpBaseClient<StripeEndpoint> {
64 protected readonly baseUrl = 'https://api.stripe.com';
65
66 constructor(private readonly apiKey: string) {
67 super(ClientVendor.Stripe);
68 }
69
70 createCustomer(body: { email: string; name: string }) {
71 return this.request<typeof body, Stripe.Customer>({
72 method: 'POST',
73 path: '/v1/customers',
74 endpoint: StripeEndpoint.CustomersCreate,
75 body,
76 });
77 }
78
79 // Weitere Endpunkte kommen hier hin.
80}

Das ist der Trick. Die Klasse versucht nicht, ganz Stripe abzubilden. Sie modelliert das HTTP-Verhalten, das wir für jeden Vendor-Aufruf haben wollen. Stripe bekommt immer noch Form-Encoding. WorkOS bekommt weiterhin Bearer-Auth und JSON-Bodies. Slack bekommt immer noch sein seltsames ok: false-Verhalten bei HTTP 200. Aber der Rest unseres Backends sieht eine einheitliche Struktur.

Du kannst hier eine längere Version unseres HttpBaseClient sehen, die generisch und leichter lesbar als Beispielcode modifiziert wurde.

Wo wir SDKs noch nutzen

Unser aktueller Ansatz ist hybrid: Zur Laufzeit nutzen wir unseren eigenen HTTP-Client, aber wir behalten SDKs dort, wo ihre Typen noch Zeit sparen. StripeHttpClient kann Stripe.Customer zurückgeben, SlackHttpClient kann die Argumenttypen von slack/web-api borgen, und WorkOS-Typen können immer noch die Wire-Antwort beschreiben.

Ich erwarte, dass wir das mit der Zeit ebenfalls auslaufen lassen. Je besser KI darin wird, die genauen Request- und Response-Typen zu generieren und zu pflegen, die wir brauchen, desto schwächer wird das Argument, ein ganzes SDK-Paket nur für Typen zu behalten. Aber das Laufzeitverhalten ist der schmerzhafte Teil, also beginnt die Migration dort.

Wir nutzen SDKs immer noch, wenn das SDK die Produktgrenze darstellt, nicht nur ein Wrapper um REST. Observability ist das klarste Beispiel. Für Sentry übernimmt das SDK die Laufzeitinstrumentierung, Fehlererfassung, Scope-Propagation, Release-Metadaten und Integrationen, die wir nicht selbst stricken wollen. Das ist etwas anderes, als ein Vendor-SDK als dünnen Client für gewöhnliche Backend-HTTP-Aufrufe zu verwenden.

Nicht jede API ist REST über HTTP, und das ist in Ordnung. Datenbankaufrufe sind ein gutes Beispiel. Unsere Abstraktion auf niedrigerer Ebene ist BaseClient: Es gibt jedem Client denselben Vertrag für Metriken, Logging und Fehlerbehandlung, während eine Kindklasse überschreiben kann, was „fetch" für ihren Transport bedeutet.

Wohin die Reise geht

Entwickler werden API-Dokumentationen als die echte Integrationsanleitung behandeln und SDKs als Referenzimplementierungen: Vorlagen für Auth, Payloads, Paginierung, Retries und Randfälle. Die nächste Version von „Ein SDK ausliefern" könnte „Eine Agenten-Fähigkeit ausliefern" sein, die Agenten lehrt, wie man die API korrekt aufruft, die richtigen Muster wiederverwendet und vermeidet, jeden Laufzeitaufruf durch ein Vendor-Paket zu schleusen.

Mit einem Klick speichern

Virale Artikel mit YouMind per KI tief lesen

Speichere die Quelle, stelle gezielte Fragen, fasse die Argumentation zusammen und verwandle einen viralen Artikel in wiederverwendbare Notizen in einem einzigen KI-Arbeitsbereich.

YouMind entdecken
Für Creator

Verwandle dein Markdown in einen sauberen 𝕏-Artikel

Wenn du eigene Langtexte veröffentlichst, wird die 𝕏-Formatierung von Bildern, Tabellen und Codeblöcken mühsam. YouMind macht aus einem ganzen Markdown-Entwurf einen sauberen, sofort postbaren 𝕏-Artikel.

Markdown zu 𝕏 testen

Mehr Muster zum Entschlüsseln

Aktuelle virale Artikel

Mehr virale Artikel entdecken