Perché abbiamo smesso di usare gli SDK

@alvinsng
INGLESE1 giorno fa · 14 lug 2026
435K
674
52
55
1.0K

TL;DR

Alvin Sng spiega perché il suo team ha sostituito gli SDK dei vendor con un client HTTP personalizzato per migliorare il debugging, unificare l'osservabilità ed eliminare l'eccesso di dipendenze.

Abbiamo smesso di usare gli SDK client di Stripe, WorkOS e Slack e stiamo migrando anche tutto il resto. Al loro posto, chiamiamo direttamente le loro API REST tramite una piccola classe wrapper che chiamiamo HttpBaseClient.

Sembra un passo indietro. Gli SDK dovrebbero farti risparmiare tempo. Ma si sente sempre più spesso parlare di AI che porta più codice a funzionare in modo nativo. Lo stesso vale per gli SDK. Perché?

  • Gli SDK nascondono i dettagli grezzi, come gli header e i corpi delle risposte HTTP, che sono fondamentali per gli agenti che devono fare debug o inviare dettagli al team upstream per le indagini.
  • Si aspettano risposte corrette, ma in realtà arrivano risposte malformate da firewall, load balancer e gateway. Questo maschera i dettagli di debug di cui abbiamo bisogno.
  • Gli SDK bypassano la necessità di un punto di ingresso centralizzato per retry, gestione degli errori e osservabilità, permettendo agli agenti di riscrivere i propri pattern.
  • Portano con sé molto gonfiore, di solito generato automaticamente da OpenAPI, quando usiamo solo un piccolo sottoinsieme di endpoint.

Pensa al motivo per cui sono nati gli SDK. Sono nati perché le aziende volevano che l'adozione fosse semplice: distribuire una libreria condivisa, far risparmiare tempo ai clienti e nascondere il lavoro di integrazione ripetitivo. Ma l'AI ha cambiato quella curva di costo. Integrare con un SDK ora richiede spesso tanto lavoro quanto chiamare direttamente l'API HTTP, ed è economico scrivere un client REST su misura per esattamente gli endpoint che usiamo, fornendo al contempo un'osservabilità unificata migliore.

Il gioco infinito del whack-a-mole

Alvin Sng - inline image

Delle magnifiche risposte "JSON"

Ecco come appariva la nostra dashboard degli errori di Sentry. Scoprivamo che una piccola percentuale di richieste falliva da qualche percorso di codice, poi facevamo una monkey-patch solo per quel percorso per gestire l'incidente dell'SDK per l'utente. Il giorno dopo un altro percorso inciampava in un errore inaspettato; facevamo la patch anche per quello, e il gioco non finiva mai.

Gli SDK sono fragili in una modalità di fallimento comune in produzione: il server dice che qualcosa è andato storto, ma la risposta non ha la forma JSON che l'SDK si aspettava.

Un gateway Nginx sovraccarico restituisce HTML inaspettato. Cloudflare blocca una richiesta. Un firewall visualizza una pagina di rate-limit. L'API del fornitore di solito è JSON, ma la cosa che le sta davanti non è sempre l'API del fornitore.

Quando succede, molti SDK provano a parsare la risposta, falliscono con un errore di parsing generico e buttano via le parti utili. Il corpo grezzo è perso. Il testo di stato è sepolto. Gli header HTTP spesso non vengono restituiti in modo utilizzabile. Se il supporto del fornitore chiede un ID richiesta da un header HTTP, siamo sfortunati perché l'SDK non lo restituisce.

Gli agenti abusano della direttezza degli SDK

Le nostre chiamate agli SDK client erano il selvaggio west. Stripe aveva un pattern. WorkOS un altro. Slack aveva le sue stranezze. Altre integrazioni avevano fetch grezzo, chiamate SDK, logica di retry personalizzata o nessuna logica di retry.

Gli SDK rendevano facile fare la cosa sbagliata. Un agente poteva sempre andare direttamente al client del fornitore e chiamare \stripe.customers.create(...)\ da qualsiasi rotta. Sembra produttivo, ma bypassa il posto condiviso dove dovrebbero stare autenticazione, retry, log, metriche e traduzione degli errori. Avevamo wrapper di chiusura come questo sparsi nel nostro codice:

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

Se ti sfuggiva un punto e dimenticavi di avvolgere la chiamata SDK, era un brutto giorno. Un rate limit di Stripe e un rate limit di WorkOS significano la stessa cosa per il nostro prodotto: l'upstream ci chiede di rallentare. Ma a livello di tipo, erano oggetti completamente diversi. Qualche codice catturava eccezioni specifiche dell'SDK. Qualche codice catturava Error generico. Qualche codice non catturava nulla. Questo si trasformava nel gioco del whack-a-mole di Sentry: risolvi un 429 in un punto di chiamata, aspetta che la stessa classe di fallimento appaia da qualche altra parte.

Gli SDK sono gonfiati

I pacchetti NPM openai e anthropic-ai/sdk sono generati automaticamente da specifiche OpenAPI da Stainless. Anche Stripe è generato dalla specifica OpenAPI di Stripe. È così che si scala la manutenzione di un SDK pubblico per una grande API in dozzine di linguaggi di programmazione.

Ma un grande SDK pubblico deve servire tutti. Il nostro backend non ha bisogno dell'SDK di tutti. Ha bisogno dei nostri otto endpoint di Stripe, degli endpoint utente e org di WorkOS e dei metodi di Slack che chiamiamo effettivamente. Stripe è 6,5 MB, workos-inc/node è 6,9 MB, slack/web-api è 7,7 MB e linear/sdk è 34 MB. All'estremo opposto, googleapis arriva a 198 MB.

Gli SDK generati portano con sé l'intera piattaforma: centinaia di metodi, overload, helper di paginazione, comportamento di retry, rilevamento dell'ambiente, shim di compatibilità e vecchie superfici che non possono scomparire perché qualcuno, da qualche parte, dipende ancora da loro. All'interno del nostro backend, quella generalità è di solito gonfiore. Peggio ancora, si frappone tra noi e il filo.

Ci dimentichiamo che le API HTTP sono contratti API

A volte le persone parlano delle API HTTP come se fossero dettagli di implementazione di basso livello e gli SDK fossero la vera interfaccia stabile. L'API REST pubblica è un contratto. I fornitori non possono romperla con leggerezza. Anche gli autori di SDK lo sanno, perché non possono obbligare ogni cliente ad aggiornare. Molti clienti continuano a usare versioni di SDK vecchie di anni in produzione, il che significa che il vecchio contratto di rete deve continuare a funzionare comunque.

Il nostro HttpBaseClient

HttpBaseClient è la nostra sostituzione per gli SDK client. Le sottoclassi dei fornitori forniscono le parti specifiche del fornitore: URL base, header di autenticazione, content type, mappatura degli errori e metodi ristretti per gli endpoint che usiamo effettivamente. HttpBaseClient possiede il resto: serializzazione, parsing, errori di trasporto, log strutturati, metriche, mappatura dello stato e tracciamento della durata. Questo unifica l'osservabilità in modo che ogni fornitore segua standard coerenti. Ecco la forma, semplificata:

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 // More endpoints go here.
80}

Questo è il trucco. La classe non cerca di modellare tutto Stripe. Modella il comportamento HTTP che vogliamo che ogni chiamata al fornitore abbia. Stripe ottiene ancora la codifica dei form. WorkOS ottiene ancora l'autenticazione bearer e i corpi JSON. Slack ottiene ancora il suo strano comportamento ok: false su HTTP 200. Ma il resto del nostro backend vede una forma coerente.

Puoi vedere una versione più lunga di come appare il nostro HttpBaseClient qui, modificata per essere generica e più facile da leggere come codice d'esempio.

Dove usiamo ancora gli SDK

Il nostro approccio attuale è ibrido: usiamo il nostro client HTTP a runtime, ma teniamo gli SDK dove i loro tipi ci fanno ancora risparmiare tempo. StripeHttpClient può restituire Stripe.Customer, SlackHttpClient può prendere in prestito i tipi di argomento di slack/web-api, e i tipi di WorkOS possono ancora descrivere la risposta di rete.

Prevedo che elimineremo gradualmente anche questo. Man mano che l'AI migliora nel generare e mantenere i tipi esatti di richiesta e risposta di cui abbiamo bisogno, il caso per tenere un intero pacchetto SDK solo per i tipi si indebolisce. Ma il comportamento a runtime è la parte dolorosa, quindi è da lì che parte la migrazione.

Usiamo ancora gli SDK quando l'SDK è il confine del prodotto, non solo un wrapper attorno a REST. L'osservabilità è l'esempio più chiaro. Per Sentry, l'SDK gestisce l'instrumentazione a runtime, la cattura degli errori, la propagazione dello scope, i metadati di rilascio e le integrazioni che non vorremmo fare a mano. Questo è diverso dall'usare un SDK di un fornitore come un client sottile per normali chiamate HTTP di backend.

Non tutte le API sono REST su HTTP, e va bene. Le chiamate al database sono un buon esempio. La nostra astrazione di livello inferiore è BaseClient: offre a ogni client lo stesso contratto di metriche, logging e gestione degli errori, permettendo a una classe figlia di sovrascrivere cosa significa "fetch" per il suo trasporto.

Dove stiamo andando

Gli sviluppatori tratteranno i documenti delle API come la vera guida all'integrazione e gli SDK come implementazioni di riferimento: template per autenticazione, payload, paginazione, retry e casi limite. La prossima versione di "distribuisci un SDK" potrebbe essere "distribuisci una skill per agenti" che insegna agli agenti come chiamare l'API correttamente, riutilizzare i pattern giusti ed evitare di spingere ogni chiamata a runtime attraverso un pacchetto del fornitore.

Salva con un clic

Leggi in profondità gli articoli virali con l’AI di YouMind

Salva la fonte, fai domande mirate, riassumi l’argomentazione e trasforma un articolo virale in note riutilizzabili in un unico spazio di lavoro AI.

Scopri YouMind
Per i creator

Trasforma il tuo Markdown in un articolo 𝕏 pulito

Quando pubblichi i tuoi testi lunghi, formattare immagini, tabelle e blocchi di codice per 𝕏 è una seccatura. YouMind trasforma un'intera bozza Markdown in un articolo 𝕏 pulito e pronto da pubblicare.

Prova Markdown verso 𝕏

Altri pattern da decodificare

Articoli virali recenti

Esplora altri articoli virali