Al construir APIs type-safe con Elysia y Bun, una de las funcionalidades más poderosas es Eden Treaty: una biblioteca cliente que proporciona seguridad de tipos end-to-end entre tu backend y frontend. Servir definiciones TypeScript de manera eficiente en diferentes entornos requiere cierta atención. Este post cubre un enfoque listo para producción para servir archivos .d.ts que funciona tanto en desarrollo como en despliegues en la nube.

El Desafío: Definiciones de Tipos en Diferentes Entornos

Eden Treaty depende de archivos de definición TypeScript (.d.ts) para proporcionar inferencia de tipos en los endpoints de tu API. Generar estos tipos on-the-fly funciona bien en desarrollo, pero crea problemas en producción:

  • Desarrollo: quieres feedback instantáneo al modificar tu API.
  • Producción: quieres respuestas rápidas y predecibles sin la sobrecarga de generación de tipos en tiempo de ejecución.

La Solución de Dos Modos

La implementación usa dos modos:

  1. Cloud Run (Producción): servir definiciones de tipos estáticas pre-generadas desde dist/types.d.ts.
  2. Desarrollo: generar tipos on-the-fly usando elysia-remote-dts.

Implementación

La Configuración de Elysia

Esta es la implementación central que maneja ambos casos:

import { swagger } from '@elysiajs/swagger';
import { dts } from 'elysia-remote-dts';
import { Elysia } from 'elysia';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';

export function createElysiaApp(options: { enableSwagger?: boolean } = {}) {
    const { enableSwagger = true } = options;

    const app = new Elysia()
        // ... tu middleware y rutas
        .use(routes);

    if (enableSwagger) {
        app.use(swagger({ path: '/docs' }));
    }

    // Sirve definiciones TypeScript para Eden Treaty
    // En Cloud Run: sirve archivo estático pre-generado desde dist/types.d.ts
    // En desarrollo: genera on-the-fly usando elysia-remote-dts
    // K_SERVICE es definido automáticamente por Google Cloud Run
    const staticTypesPath = path.resolve(process.cwd(), 'dist/types.d.ts');
    const isCloudRun = !!process.env.K_SERVICE;
    const hasStaticTypes = existsSync(staticTypesPath);

    if (isCloudRun && hasStaticTypes) {
        const staticTypes = readFileSync(staticTypesPath, 'utf-8');

        app.get('/types.d.ts', ({ set }) => {
            set.headers['Content-Type'] = 'application/typescript';
            return staticTypes;
        });
    } else {
        app.use(dts('./src/index.ts', { dtsPath: '/types.d.ts' }));
    }

    return app;
}

Decisiones de Diseño Clave

1. Detección de Entorno con K_SERVICE

Google Cloud Run define automáticamente la variable de entorno K_SERVICE con el nombre del servicio. Esta es una forma confiable de detectar producción sin tener que definir manualmente NODE_ENV:

const isCloudRun = !!process.env.K_SERVICE;

2. Verificación de Existencia del Archivo Estático

Antes de intentar servir los tipos estáticos, verificamos que el archivo exista:

const hasStaticTypes = existsSync(staticTypesPath);

Esto evita un fallo si el archivo estático no fue generado por algún motivo. El sistema hace fallback a la generación on-the-fly automáticamente.

3. Header Content-Type

Al servir el archivo .d.ts, establecemos el content type apropiado:

set.headers['Content-Type'] = 'application/typescript';

Esto garantiza que los navegadores y herramientas interpreten correctamente la respuesta como TypeScript.

Generación de Tipos en Tiempo de Build con Docker

La generación de tipos ocurre una vez durante el build de Docker, no en cada solicitud:

FROM oven/bun:alpine
WORKDIR /usr/src/app

COPY . .

RUN bun install

# Genera types.d.ts en tiempo de build (en lugar de on-the-fly)
RUN bun run generate-types

# Establece la zona horaria
RUN apk add --no-cache tzdata zip && \
    cp /usr/share/zoneinfo/US/Pacific /etc/localtime && \
    echo "US/Pacific" > /etc/timezone && \
    apk del tzdata

ENTRYPOINT [ "bun", "run", "src/index.ts" ]

El Script de Generación de Tipos

Agrega un script a tu package.json:

{
  "scripts": {
    "generate-types": "bun run scripts/generate-types.ts"
  }
}

Y el script correspondiente:

// scripts/generate-types.ts
import { $ } from 'bun';

// Genera definiciones TypeScript para Eden Treaty
await $`bunx elysia-remote-dts generate ./src/index.ts -o ./dist/types.d.ts`;

console.log('Types generated successfully at dist/types.d.ts');

Beneficios de Este Enfoque

Cero Sobrecarga en Tiempo de Ejecución en Producción

La generación de tipos ocurre una vez en tiempo de build. El tiempo de respuesta es consistente, el uso de memoria es menor (sin engine de generación de tipos cargada) y no hay parsing de AST por solicitud.

Actualizaciones Instantáneas en Desarrollo

elysia-remote-dts genera tipos on-the-fly durante el desarrollo. No se requiere ningún paso de build: simplemente inicia el servidor y los tipos se actualizan a medida que cambias tu API.

Fallback Elegante

La lógica condicional maneja los casos extremos de forma limpia. Si los tipos estáticos no fueron generados, el sistema hace fallback a la generación on-the-fly. Esto también cubre el desarrollo local sin las variables de entorno de Cloud Run y los entornos de staging.

Usando Eden Treaty en el Cliente

Con los tipos siendo servidos, tu frontend puede consumirlos:

import { edenTreaty } from '@elysiajs/eden';
import type { App } from 'your-api-types'; // o buscar desde /types.d.ts

const api = edenTreaty<App>('https://your-api.run.app');

// ¡Seguridad de tipos completa!
const { data, error } = await api.users.get();
//     ^? User[]

Para la búsqueda dinámica de tipos, configura tu IDE o herramientas de build para buscar los tipos desde el endpoint /types.d.ts.

Visión General de la Arquitectura

┌─────────────────────────────────────────────────────────────────────┐
│                        Request Flow                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Client Request: GET /types.d.ts                                     │
│           │                                                          │
│           ▼                                                          │
│  ┌─────────────────┐                                                 │
│  │ Is Cloud Run?   │                                                 │
│  │ (K_SERVICE set) │                                                 │
│  └────────┬────────┘                                                 │
│           │                                                          │
│     YES   │   NO                                                     │
│     ┌─────┴─────┐                                                    │
│     ▼           ▼                                                    │
│  ┌──────────┐  ┌──────────────┐                                      │
│  │ Static   │  │ On-the-fly   │                                      │
│  │ File     │  │ Generation   │                                      │
│  │ (fast)   │  │ (elysia-dts) │                                      │
│  └────┬─────┘  └──────┬───────┘                                      │
│       │               │                                              │
│       └───────┬───────┘                                              │
│               ▼                                                      │
│        Response: types.d.ts                                          │
│        Content-Type: application/typescript                          │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Pipeline de Build

┌─────────────────────────────────────────────────────────────────────┐
│                     Docker Build Process                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. COPY source files                                                │
│           │                                                          │
│           ▼                                                          │
│  2. RUN bun install                                                  │
│           │                                                          │
│           ▼                                                          │
│  3. RUN bun run generate-types    ◄── Types generated HERE           │
│           │                                                          │
│           ▼                                                          │
│  4. dist/types.d.ts created                                          │
│           │                                                          │
│           ▼                                                          │
│  5. Image ready with pre-generated types                             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Comparación de Rendimiento

Métrica Generación On-the-fly Servicio de Archivo Estático
Tiempo de Respuesta 50-200ms 1-5ms
Uso de Memoria Mayor (engine de tipos) Mínimo
Uso de CPU Parsing por solicitud Ninguno
Impacto en Cold Start Significativo Insignificante

Buenas Prácticas

1. Versiona Tus Tipos

Incluye un comentario de versión en los tipos generados:

// scripts/generate-types.ts
import { $ } from 'bun';
import { version } from '../package.json';

const header = `// Generated types for API v${version}\n// Generated at: ${new Date().toISOString()}\n\n`;
// ... genera y antepone el header

2. Headers de Cache

Agrega headers de cache para producción:

if (isCloudRun && hasStaticTypes) {
    app.get('/types.d.ts', ({ set }) => {
        set.headers['Content-Type'] = 'application/typescript';
        set.headers['Cache-Control'] = 'public, max-age=3600'; // 1 hora
        return staticTypes;
    });
}

3. Integración con CI/CD

Verifica que los tipos se generen correctamente en tu pipeline de CI:

# .github/workflows/deploy.yml
- name: Generate and verify types
  run: |
    bun run generate-types
    test -f dist/types.d.ts || exit 1

Conclusión

Servir definiciones TypeScript para Eden Treaty se resume en una decisión: generar en tiempo de build para producción, generar on-the-fly para desarrollo. La implementación de modo dual maneja esto de forma limpia leyendo la variable de entorno K_SERVICE que Cloud Run define automáticamente.

El resultado: el desarrollo se mantiene rápido y preciso (los tipos siempre reflejan tu API actual), mientras que producción sirve archivos estáticos en 1 a 5ms en lugar de 50 a 200ms. Ambos entornos mantienen seguridad de tipos completa para los consumidores de la API.

El mismo patrón se aplica a cualquier escenario donde necesitas comportamiento específico por entorno para servir artefatos generados: detecta el entorno de forma confiable y ten un fallback claro.


Logro Técnico: Servicio de tipos con conciencia del entorno que reduce los tiempos de respuesta en un 95% en producción mientras mantiene total flexibilidad en desarrollo.

Tecnologías Clave: Elysia, Bun, Eden Treaty, Google Cloud Run, Docker, TypeScript