Por qué dejamos de usar SDKs

@alvinsng
INGLÉShace 1 día · 14 jul 2026
435K
674
52
55
1.0K

TL;DR

Alvin Sng explica por qué su equipo reemplazó los SDKs de proveedores por un cliente HTTP personalizado para mejorar la depuración, unificar la observabilidad y eliminar la sobrecarga de dependencias.

Dejamos de usar los SDKs de Stripe, WorkOS y Slack, y estamos migrando el resto. En su lugar, llamamos directamente a sus APIs REST a través de una pequeña clase envolvente llamada HttpBaseClient.

Suena contradictorio. Se supone que los SDKs te ahorran tiempo. Pero has oído que la IA está migrando más código para que se ejecute de forma nativa. La misma idea aplica a los SDKs. ¿Por qué?

  • Los SDKs ocultan detalles crudos, como los encabezados de respuesta HTTP y los cuerpos de respuesta sin procesar, que son críticos para agentes que depuran problemas o envían detalles al equipo upstream para investigación.
  • Esperan respuestas correctas, pero en realidad llegan respuestas mal formadas de firewalls, balanceadores de carga y gateways. Eso oculta los detalles de depuración que necesitamos.
  • Los SDKs evitan la función forzosa de un punto de entrada centralizado para reintentos, manejo de errores y observabilidad, lo que permite que los agentes reescriban sus propios patrones.
  • Traen una pesada hinchazón, generalmente auto-generada de OpenAPI, cuando solo usamos un pequeño subconjunto de endpoints.

Piensa por qué surgieron los SDKs. Nacieron porque las empresas querían que la adopción se sintiera fácil: compartir una biblioteca, ahorrar tiempo a los clientes y ocultar el trabajo repetitivo de integración. Pero la IA cambió esa curva de costo. Integrar con un SDK ahora suele ser tanto trabajo como llamar directamente a la API HTTP, y es barato escribir un cliente REST adaptado exactamente a los endpoints que usamos, proporcionando una mejor observabilidad unificada.

El interminable juego de golpear topos

Alvin Sng - inline image

Qué "JSON" tan bonito

Así se veía nuestro panel de errores de Sentry. Encontrábamos que un pequeño porcentaje de solicitudes fallaba desde alguna ruta de código, luego aplicábamos un monkey-patch solo a esa ruta para manejar el percance del SDK. Cada día otra ruta tropezaba con un error inesperado; parcheábamos esa también, y el juego nunca terminaba.

Los SDKs son frágiles en un modo común de fallo en producción: el servidor dice que algo salió mal, pero la respuesta no tiene la forma JSON que el SDK esperaba.

Un gateway Nginx sobrecargado devuelve HTML inesperado. Cloudflare bloquea una solicitud. Un firewall muestra una página de límite de tasa. La API del proveedor suele ser JSON, pero lo que está delante no siempre es la API del proveedor.

Cuando eso ocurre, muchos SDKs intentan analizar la respuesta, fallan con un error genérico de análisis y descartan las partes útiles. El cuerpo crudo desaparece. El texto de estado queda oculto. Los encabezados HTTP a menudo no se devuelven de forma utilizable. Si el soporte del proveedor pide un ID de solicitud de un encabezado HTTP, no tenemos suerte porque el SDK no lo devuelve.

Los agentes abusan del acceso directo de los SDKs

Nuestras llamadas a los SDKs cliente eran el salvaje oeste. Stripe tenía un patrón. WorkOS tenía otro. Slack tenía sus propias peculiaridades. Otras integraciones tenían fetch crudo, llamadas SDK, lógica de reintentos única o ninguna lógica de reintentos.

Los SDKs hacían que lo incorrecto pareciera fácil. Un agente siempre podía ir directamente al cliente del proveedor y llamar a stripe.customers.create(...) desde cualquier ruta. Eso parece productivo, pero elude el lugar compartido donde deberían vivir la autenticación, los reintentos, las métricas, los registros y la traducción de errores. Teníamos envoltorios de clausura como este esparcidos en nuestra base de código:

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

Si te perdías un lugar y olvidabas envolver la llamada al SDK, tenías un mal día. Un límite de tasa de Stripe y uno de WorkOS significan lo mismo para nuestro producto: el upstream nos pide que reduzcamos la velocidad. Pero a nivel de tipo, eran objetos totalmente diferentes. Algo de código atrapaba excepciones específicas del SDK. Algo atrapaba Error genérico. Algo no atrapaba nada. Eso se convertía en el golpeteo de topos de Sentry: arreglar un 429 en un sitio de llamada, esperar la misma clase de fallo en otro lugar.

Los SDKs son pesados

Los paquetes NPM openai y anthropic-ai/sdk se auto-generan a partir de especificaciones OpenAPI por Stainless. Stripe también se genera desde la especificación OpenAPI de Stripe. Así es como se escala el mantenimiento de un SDK público para una API grande en docenas de lenguajes de programación.

Pero un gran SDK público tiene que servir a todos. Nuestro backend no necesita el SDK de todos. Necesita nuestros ocho endpoints de Stripe, los endpoints de usuario y organización de WorkOS, y los métodos de Slack que realmente llamamos. Stripe pesa 6.5 MB, workos-inc/node pesa 6.9 MB, slack/web-api pesa 7.7 MB, y linear/sdk pesa 34 MB. En el extremo, googleapis alcanza los 198 MB.

Los SDKs generados traen toda la plataforma consigo: cientos de métodos, sobrecargas, ayudantes de paginación, comportamiento de reintentos, detección de entorno, shims de compatibilidad y superficies antiguas que no pueden desaparecer porque alguien, en algún lugar, aún depende de ellas. Dentro de nuestro propio backend, esa generalidad suele ser pesadez. Peor aún, se interpone entre nosotros y el cable.

Olvidamos que las APIs HTTP son contratos de API

A veces la gente habla de las APIs HTTP como si fueran detalles de implementación de bajo nivel y los SDKs la interfaz estable real. La API REST pública es un contrato. Los proveedores no pueden romperlo casualmente. Los autores de SDKs también lo saben, porque no pueden obligar a cada cliente a actualizar. Muchos clientes mantienen versiones de SDK de hace años en producción, lo que significa que el antiguo contrato de cable debe seguir funcionando de todos modos.

Nuestro propio HttpBaseClient

HttpBaseClient es nuestro reemplazo para los SDKs cliente. Las subclases de proveedores proporcionan las piezas específicas del proveedor: URL base, encabezados de autenticación, tipo de contenido, mapeo de errores y métodos estrechos para los endpoints que realmente usamos. HttpBaseClient se encarga del resto: serialización, análisis, errores de transporte, registros estructurados, métricas, mapeo de estado y seguimiento de duración. Esto unifica la observabilidad para que cada proveedor siga estándares consistentes. Aquí está la 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}

Ese es el truco. La clase no intenta modelar todo Stripe. Modela el comportamiento HTTP que queremos que tenga cada llamada al proveedor. Stripe aún obtiene codificación de formularios. WorkOS aún obtiene autenticación bearer y cuerpos JSON. Slack aún obtiene su extraño comportamiento ok: false en HTTP 200. Pero el resto de nuestro backend ve una forma consistente.

Puedes ver una versión más larga de cómo se ve nuestro HttpBaseClient aquí, modificada para que sea genérica y más fácil de leer como código de ejemplo.

Donde todavía usamos SDKs

Nuestro enfoque actual es híbrido: usar nuestro propio cliente HTTP en tiempo de ejecución, pero mantener los SDKs cerca donde sus tipos aún ahorran tiempo. StripeHttpClient puede devolver Stripe.Customer, SlackHttpClient puede tomar prestados los tipos de argumentos de slack/web-api, y los tipos de WorkOS aún pueden describir la respuesta del cable.

Espero que esto también se elimine gradualmente con el tiempo. A medida que la IA mejore en generar y mantener los tipos exactos de solicitud y respuesta que necesitamos, el caso de mantener un paquete SDK completo solo por los tipos se debilita. Pero el comportamiento en tiempo de ejecución es la parte dolorosa, así que ahí comienza la migración.

Todavía usamos SDKs cuando el SDK es el límite del producto, no solo un envoltorio alrededor de REST. La observabilidad es el ejemplo más claro. Para Sentry, el SDK maneja la instrumentación en tiempo de ejecución, la captura de errores, la propagación de alcance, los metadatos de versión y las integraciones que no querríamos hacer manualmente. Eso es diferente de usar un SDK de proveedor como un cliente fino para llamadas HTTP comunes de backend.

No todas las APIs son REST sobre HTTP, y está bien. Las llamadas a bases de datos son un buen ejemplo. Nuestra abstracción de nivel inferior es BaseClient: le da a cada cliente el mismo contrato de métricas, registro y manejo de errores, permitiendo que una clase hija anule lo que significa "fetch" para su transporte.

Hacia dónde nos dirigimos

Los desarrolladores tratarán la documentación de la API como la guía de integración real y los SDKs como implementaciones de referencia: plantillas para autenticación, cargas útiles, paginación, reintentos y casos límite. La próxima versión de "entregar un SDK" podría ser "entregar una habilidad de agente" que enseñe a los agentes cómo llamar a la API correctamente, reutilizar los patrones adecuados y evitar pasar cada llamada en tiempo de ejecución a través de un paquete de proveedor.

Guardar con un clic

Lee artículos virales en profundidad con IA en YouMind

Guarda la fuente, haz preguntas concretas, resume el argumento y convierte un artículo viral en notas reutilizables en un único espacio de trabajo con IA.

Explora YouMind
Para creadores

Convierte tu Markdown en un artículo de 𝕏 impecable

Cuando publicas tus propios textos largos, dar formato en 𝕏 a imágenes, tablas y bloques de código es un fastidio. YouMind convierte un borrador completo en Markdown en un artículo de 𝕏 impecable y listo para publicar.

Prueba Markdown a 𝕏

Más patrones por descifrar

Artículos virales recientes

Explorar más artículos virales