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

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:
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:
1abstract class HttpBaseClient<TEndpoint extends string> {2 protected abstract readonly baseUrl: string;34 protected constructor(private readonly dependency: string) {}56 protected abstract buildAuthHeaders(): Promise<Record<string, string>>;78 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();2223 logInfo('iniciando solicitud upstream', labels);2425 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 labels35 );3637 const body = await parseBody(response);38 if (!response.ok) throw this.mapHttpError(response, body);3940 logInfo('solicitud upstream exitosa', {41 ...labels,42 statusCode: response.status,43 durationMs: performance.now() - start,44 });4546 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}5758// Entonces un envoltorio para Stripe se vuelve pequeño y explícito:59enum StripeEndpoint {60 CustomersCreate = 'stripe/customers/create',61}6263class StripeHttpClient extends HttpBaseClient<StripeEndpoint> {64 protected readonly baseUrl = 'https://api.stripe.com';6566 constructor(private readonly apiKey: string) {67 super(ClientVendor.Stripe);68 }6970 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 }7879 // 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.





