Por que paramos de usar SDKs

@alvinsng
INGLÊShá 1 dia · 14 de jul. de 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 inchaço de dependências.

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

Isso parece ao contrário. Espera-se que os SDKs economizem tempo. Mas você tem ouvido falar sobre IA portando mais código para rodar nativamente. A mesma ideia se aplica aos SDKs. Por quê?

  • Os SDKs escondem os detalhes brutos, como cabeçalhos de resposta HTTP e corpos de resposta brutos, 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 chegam de firewalls, balanceadores de carga e gateways. Isso mascara os detalhes de depuração que precisamos.
  • Os SDKs ignoram a função de forçar um ponto de entrada centralizado para retentativas, tratamento de erros e observabilidade, permitindo que os agentes reescrevam seus próprios padrões.
  • Eles carregam um peso morto pesado, geralmente gerado automaticamente a partir de OpenAPI, quando usamos apenas um pequeno subconjunto de endpoints.

Pense em por que 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 de integração repetitivo. Mas a IA mudou essa curva de custo. Integrar com um SDK agora é muitas vezes tanto trabalho quanto chamar a API HTTP diretamente, e é barato escrever um cliente REST personalizado exatamente para os endpoints que usamos, ao mesmo tempo que oferece melhor observabilidade unificada.

O jogo interminável de bater na toupeira

Alvin Sng - inline image

Respostas "JSON" tão adoráveis

Era assim que nosso painel de erros do Sentry costumava ser. Descobríamos que uma pequena porcentagem de requisições falhava em algum caminho de código, então fazíamos um monkey-patch apenas naquele caminho para lidar com o erro do SDK para o usuário. A cada dia, outro caminho de código causava um erro inesperado; corrigíamos também aquele, e o jogo nunca terminava.

Os 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 está no formato JSON que o SDK esperava.

Um gateway Nginx sobrecarregado retorna HTML inesperado. Cloudflare bloqueia uma requisição. Um firewall exibe uma página de limitação 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 desaparece. O texto de status é 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 simplicidade direta dos SDKs

Nossas chamadas de SDK cliente eram o Velho Oeste. Stripe tinha um padrão. WorkOS tinha outro. 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 faziam a coisa errada parecer fácil. Um agente podia sempre 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, retentativas, métricas, logs 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, você tinha um dia ruim. Uma limitação de taxa do Stripe e uma limitação de taxa do WorkOS significam a mesma coisa para o nosso produto: o upstream está pedindo para desacelerarmos. 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.

Os SDKs são inchados

Os pacotes NPM openai e anthropic-ai/sdk são gerados automaticamente a partir de especificações OpenAPI pela Stainless. 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. 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 junto: centenas de métodos, sobrecargas, ajudantes de paginação, comportamento de retentativa, 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 as 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 os SDKs fossem a interface estável real. A API REST pública é um contrato. Os fornecedores não podem quebrá-la casualmente. Os autores de SDK também sabem disso, porque não podem forçar todos os clientes a atualizar. Muitos clientes continuam executando versões antigas do SDK em produção, o que significa que o antigo contrato de fio tem que continuar funcionando de qualquer maneira.

Nosso próprio HttpBaseClient

HttpBaseClient é nossa substituição para os SDKs clientes. Subclasses de provedor fornecem as peças específicas do fornecedor: URL base, cabeçalhos de autenticação, tipo de conteúdo, mapeamento de erros e métodos estreitos para os endpoints que realmente usamos. HttpBaseClient possui o 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á o formato, simplificado:

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. Stripe ainda recebe form encoding. WorkOS ainda recebe autenticação bearer e corpos JSON. Slack ainda recebe seu comportamento estranho de ok: false em 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. StripeHttpClient pode retornar Stripe.Customer, SlackHttpClient pode pegar emprestados os tipos de argumento de slack/web-api, e os tipos do WorkOS ainda podem descrever a resposta do fio.

Espero que eliminemos isso com o tempo também. À medida que a IA fica melhor em gerar e manter os tipos exatos de requisição e resposta 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 Sentry, o SDK lida com instrumentação de tempo de execução, captura de erros, propagação de escopo, metadados de versão e integrações que não gostaríamos de fazer 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 nível inferior é BaseClient: ela fornece a cada cliente as mesmas métricas, logs e contrato de 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 a documentação da API como o guia de integração real e os SDKs como implementações de referência: modelos para autenticação, payloads, paginação, retentativas 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 através de um pacote de fornecedor.

Salvar com um clique

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

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

Explorar o YouMind
Para criadores

Transforme seu Markdown em um artigo 𝕏 impecável

Quando você publica 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 em um artigo 𝕏 impecável e pronto para publicar.

Experimente Markdown para 𝕏

Mais padrões para decifrar

Artigos virais recentes

Explorar mais artigos virais