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 el exceso de dependencias.

Dejamos de usar los SDK de cliente de Stripe, WorkOS y Slack, y estamos en proceso de migrar el resto. En su lugar, llamamos directamente a sus APIs REST a través de una pequeña clase envoltorio que llamamos HttpBaseClient.

Eso suena al revés. Se supone que los SDK te ahorran tiempo. Pero has oído hablar de que la IA está migrando más código para que se ejecute de forma nativa. Lo mismo aplica a los SDK. ¿Por qué?

  • Los SDK ocultan los detalles brutos, como los encabezados de respuesta HTTP y los cuerpos de respuesta sin procesar, que son críticos para los agentes que están depurando problemas o enviando detalles al equipo upstream para investigación.
  • Esperan respuestas correctas, pero en la realidad, cortafuegos, balanceadores de carga y puertas de enlace devuelven respuestas malformadas. Eso enmascara los detalles de depuración que necesitamos.
  • Los SDK 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 mucho peso muerto, generalmente generado automáticamente a partir de OpenAPI, cuando solo usamos un subconjunto pequeño de endpoints.

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

El interminable juego del golpe al topo

Alvin Sng - inline image

Tan "hermosas" respuestas JSON

Así solía verse 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 parche mono a esa ruta para manejar el percance del SDK por el usuario. Al día siguiente, otra ruta de código tropezaba con un error inesperado; parcheábamos también esa, y el juego nunca terminaba.

Los SDK son frágiles en un modo de fallo común 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 cortafuegos muestra una página de limitación de velocidad. La API del proveedor suele ser JSON, pero lo que está delante no siempre es la API del proveedor.

Cuando eso ocurre, muchos SDK intentan analizar la respuesta, fallan con un error de análisis genérico y desechan las partes útiles. El cuerpo sin procesar se pierde. El texto de estado queda enterrado. Los encabezados HTTP a menudo no se devuelven de manera 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 de la inmediatez de los SDK

Nuestras llamadas al SDK del cliente eran el salvaje oeste. Stripe tenía un patrón. WorkOS tenía otro. Slack tenía sus propias peculiaridades. Otras integraciones usaban fetch sin procesar, llamadas al SDK, lógica de reintento particular, o ninguna lógica de reintento en absoluto.

Los SDK 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 cierre como este esparcidos por nuestro código base:

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

Si te saltabas un lugar y olvidabas envolver la llamada al SDK, tenías un mal día. Un límite de velocidad de Stripe y un límite de velocidad 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 de código atrapaba Error genérico. Algo de código no atrapaba nada. Eso se convertía en el juego del golpe al topo en Sentry: arreglar un 429 en un sitio de llamada, esperar la misma clase de fallo en otro lugar.

Los SDK son inflados

Los paquetes NPM openai y anthropic-ai/sdk se generan automáticamente a partir de especificaciones OpenAPI por Stainless. Stripe también se genera a partir de 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 SDK generados traen toda la plataforma con ellos: cientos de métodos, sobrecargas, ayudantes de paginación, comportamiento de reintento, detección de entorno, shims de compatibilidad y superficies antiguas que no pueden desaparecer porque alguien, en algún lugar, todavía depende de ellas. Dentro de nuestro propio backend, esa generalidad suele ser peso muerto. 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 SDK fueran la interfaz estable real. La API REST pública es un contrato. Los proveedores no pueden romperla casualmente. Los autores de SDK también lo saben, porque no pueden obligar a todos los clientes a actualizar. Muchos clientes siguen ejecutando versiones antiguas del SDK 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 SDK de cliente. Las subclases de proveedor 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 posee el 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('iniciando solicitud upstream', 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('solicitud upstream exitosa', {
41 ...labels,
42 statusCode: response.status,
43 durationMs: performance.now() - start,
44 });
45
46 return body as TResponse;
47 } catch (cause) {
48 logWarn('solicitud upstream fallida', {
49 ...labels,
50 durationMs: performance.now() - start,
51 cause,
52 });
53 throw cause;
54 }
55 }
56}
57
58// Entonces un envoltorio para Stripe se vuelve pequeño y explícito:
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 // Aquí van más endpoints.
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 sigue usando codificación de formularios. WorkOS sigue usando autenticación bearer y cuerpos JSON. Slack sigue manteniendo 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 ser genérica y más fácil de leer como código de ejemplo.

Dónde seguimos usando SDK

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

Espero que eliminemos esto también 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 por ahí comienza la migración.

Todavía usamos SDK 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 implementar desde cero. Eso es diferente de usar un SDK de proveedor como un cliente fino para llamadas HTTP ordinarias de backend.

No toda API es 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, mientras permite 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 SDK como implementaciones de referencia: plantillas para autenticación, cargas útiles, paginación, reintentos y casos límite. La próxima versión de "enviar un SDK" puede ser "enviar una habilidad de agente" que enseñe a los agentes cómo llamar correctamente a la API, reutilizar los patrones adecuados y evitar enviar 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