Por que paramos de usar SDKs

@alvinsng
INGLÊShá 1 dia · 14/07/2026
435K
674
52
55
1.0K

TL;DR

Alvin Sng explica por que sua equipe substituiu os SDKs de fornecedores por um cliente HTTP personalizado para melhorar a depuração, unificar a observabilidade e eliminar o excesso de dependências.

Paramos de usar os SDKs de cliente do Stripe, WorkOS e Slack e estamos em processo de migrar o restante. Em vez disso, chamamos suas APIs REST diretamente por meio de uma pequena classe wrapper que chamamos de HttpBaseClient.

Isso parece um retrocesso. SDKs deveriam economizar seu tempo. Mas você tem ouvido sobre IA portar mais código para ser executado nativamente. A mesma ideia se aplica a SDKs. Por quê?

  • Os SDKs escondem os detalhes brutos, como cabeçalhos de resposta HTTP e corpos de resposta crus, que são essenciais para agentes que estão depurando problemas ou enviando detalhes para a equipe upstream investigar.
  • Eles esperam respostas adequadas, mas na realidade, respostas malformadas vêm de firewalls, balanceadores de carga e gateways. Isso mascara os detalhes de depuração de que precisamos.
  • Os SDKs ignoram a função de ter um ponto de entrada centralizado para repetições, tratamento de erros e observabilidade, o que permite que os agentes reescrevam seus próprios padrões.
  • Eles carregam um peso morto considerável, geralmente gerado automaticamente a partir do OpenAPI, quando usamos apenas um subconjunto minúsculo de endpoints.

Pense no motivo pelo qual os SDKs surgiram. Eles nasceram porque as empresas queriam que a adoção parecesse fácil: enviar uma biblioteca compartilhada, economizar tempo dos clientes e esconder o trabalho repetitivo de integração. Mas a IA mudou essa curva de custo. Integrar com um SDK agora dá tanto trabalho quanto chamar a API HTTP diretamente, e é barato escrever um cliente REST personalizado exatamente para os endpoints que usamos, fornecendo uma observabilidade unificada melhor.

O jogo infinito de bater na toupeira

Alvin Sng - inline image

Respostas "JSON" tão adoráveis

Era assim que nosso painel de erros do Sentry costumava se parecer. Encontrávamos que uma pequena porcentagem das requisições falhava em algum caminho de código, então fazíamos um monkey-patch apenas naquele caminho para lidar com o contratempo do SDK para o usuário. No dia seguinte, outro caminho de código tropeçava em um erro inesperado; corrigíamos esse também, e o jogo nunca terminava.

SDKs são frágeis em um modo comum de falha em produção: o servidor diz que algo deu errado, mas a resposta não tem a forma JSON que o SDK esperava.

Um gateway Nginx sobrecarregado retorna HTML inesperado. O Cloudflare bloqueia uma requisição. Um firewall exibe uma página de limite de taxa. A API do fornecedor geralmente é JSON, mas a coisa na frente dela nem sempre é a API do fornecedor.

Quando isso acontece, muitos SDKs tentam analisar a resposta, falham com um erro de análise genérico e descartam as partes úteis. O corpo bruto se foi. O texto de status está enterrado. Os cabeçalhos HTTP geralmente não são retornados de forma utilizável. Se o suporte do fornecedor pedir um ID de requisição de um cabeçalho HTTP, estamos sem sorte porque o SDK não o retorna.

Agentes abusam da direção dos SDKs

Nossas chamadas de SDK de cliente eram o velho oeste. O Stripe tinha um padrão. O WorkOS tinha outro. O Slack tinha suas próprias peculiaridades. Outras integrações tinham fetch bruto, chamadas de SDK, lógica de repetição avulsa ou nenhuma lógica de repetição.

Os SDKs tornavam a coisa errada fácil. Um agente sempre podia ir diretamente ao cliente do fornecedor e chamar stripe.customers.create(...) de qualquer rota. Isso parece produtivo, mas ignora o lugar compartilhado onde autenticação, repetições, logs, métricas e tradução de erros deveriam estar. Tínhamos wrappers de fechamento como este espalhados em nossa base de código:

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

Se você perdesse um ponto e esquecesse de envolver a chamada do SDK, tinha um dia ruim. Um limite de taxa do Stripe e um limite de taxa do WorkOS significam a mesma coisa para nosso produto: o upstream está nos pedindo para desacelerar. Mas no nível de tipo, eles eram objetos totalmente diferentes. Algum código capturava exceções específicas do SDK. Algum capturava Error genérico. Algum não capturava nada. Isso se transformava no jogo de bater na toupeira do Sentry: corrigir um 429 em um local de chamada, esperar pela mesma classe de falha em outro lugar.

SDKs são inchados

Os pacotes NPM openai e anthropic-ai/sdk são gerados automaticamente a partir de especificações OpenAPI pela Stainless. O Stripe também é gerado a partir da especificação OpenAPI do Stripe. É assim que se escala a manutenção de um SDK público para uma API grande em dezenas de linguagens de programação.

Mas um ótimo SDK público tem que servir a todos. Nosso backend não precisa do SDK de todos. Ele precisa dos nossos oito endpoints do Stripe, dos endpoints de usuário e organização do WorkOS e dos métodos do Slack que realmente chamamos. O Stripe tem 6,5 MB, workos-inc/node tem 6,9 MB, slack/web-api tem 7,7 MB e linear/sdk tem 34 MB. No extremo, googleapis chega a 198 MB.

SDKs gerados trazem toda a plataforma com eles: centenas de métodos, sobrecargas, auxiliares de paginação, comportamento de repetição, detecção de ambiente, shims de compatibilidade e superfícies antigas que não podem desaparecer porque alguém, em algum lugar, ainda depende delas. Dentro do nosso próprio backend, essa generalidade geralmente é inchaço. Pior, ela fica entre nós e o fio.

Esquecemos que APIs HTTP são contratos de API

Às vezes as pessoas falam sobre APIs HTTP como se fossem detalhes de implementação de baixo nível e que os SDKs são a interface estável real. A API REST pública é um contrato. Os fornecedores não podem quebrá-la casualmente. Os autores de SDKs também sabem disso, porque não podem forçar todos os clientes a atualizar. Muitos clientes continuam executando versões de SDK de anos atrás em produção, o que significa que o antigo contrato de fio tem que continuar funcionando de qualquer maneira.

Nosso próprio HttpBaseClient

O HttpBaseClient é nossa substituição para os SDKs de cliente. Subclasses de provedor fornecem as partes específicas do fornecedor: URL base, cabeçalhos de autenticação, tipo de conteúdo, mapeamento de erros e métodos restritos para os endpoints que realmente usamos. O HttpBaseClient é dono do resto: serialização, análise, erros de transporte, logs estruturados, métricas, mapeamento de status e rastreamento de duração. Isso unifica a observabilidade para que cada fornecedor siga padrões consistentes. Aqui está a forma, simplificada:

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}

Esse é o truque. A classe não tenta modelar todo o Stripe. Ela modela o comportamento HTTP que queremos que toda chamada de fornecedor tenha. O Stripe ainda recebe codificação de formulário. O WorkOS ainda recebe autenticação bearer e corpos JSON. O Slack ainda recebe seu comportamento estranho de ok: false no HTTP 200. Mas o resto do nosso backend vê uma forma consistente.

Você pode ver uma versão mais longa de como nosso HttpBaseClient se parece aqui, modificada para ser genérica e mais fácil de ler como código de exemplo.

Onde ainda usamos SDKs

Nossa abordagem atual é híbrida: usar nosso próprio cliente HTTP em tempo de execução, mas manter os SDKs por perto onde seus tipos ainda economizam tempo. O StripeHttpClient pode retornar Stripe.Customer, o SlackHttpClient pode pegar emprestados os tipos de argumento de slack/web-api, e os tipos do WorkOS ainda podem descrever a resposta de fio.

Espero que eliminemos isso também com o tempo. À medida que a IA fica melhor em gerar e manter os tipos exatos de requisição e resposta de que precisamos, o caso de manter um pacote SDK inteiro apenas para tipos fica mais fraco. Mas o comportamento em tempo de execução é a parte dolorosa, então é por aí que a migração começa.

Ainda usamos SDKs quando o SDK é o limite do produto, não apenas um wrapper em torno de REST. Observabilidade é o exemplo mais claro. Para o Sentry, o SDK lida com instrumentação em tempo de execução, captura de erros, propagação de escopo, metadados de versão e integrações que não gostaríamos de implementar manualmente. Isso é diferente de usar um SDK de fornecedor como um cliente fino para chamadas HTTP comuns de backend.

Nem toda API é REST sobre HTTP, e tudo bem. Chamadas de banco de dados são um bom exemplo. Nossa abstração de baixo nível é o BaseClient: ele dá a cada cliente o mesmo contrato de métricas, registro e tratamento de erros, enquanto permite que uma classe filha substitua o que "fetch" significa para seu transporte.

Para onde estamos indo

Os desenvolvedores tratarão as documentações de API como o guia de integração real e os SDKs como implementações de referência: modelos para autenticação, cargas úteis, paginação, repetições e casos extremos. A próxima versão de "enviar um SDK" pode ser "enviar uma habilidade de agente" que ensina os agentes a chamar a API corretamente, reutilizar os padrões certos e evitar empurrar cada chamada de tempo de execução por meio de um pacote de fornecedor.

Guardar com um clique

Faça leitura aprofundada de artigos virais com IA no YouMind

Guarde a fonte, faça perguntas específicas, resuma o argumento e transforme um artigo viral em notas reutilizáveis num único espaço de trabalho com IA.

Explorar o YouMind
Para criadores

Transforme o seu Markdown num artigo 𝕏 impecável

Quando publica os seus próprios textos longos, formatar imagens, tabelas e blocos de código para o 𝕏 é uma dor de cabeça. O YouMind transforma um rascunho completo em Markdown num artigo 𝕏 impecável e pronto a publicar.

Experimente Markdown para 𝕏

Mais padrões para decifrar

Artigos virais recentes

Explorar mais artigos virais