When building type-safe APIs with Elysia and Bun, one of the most powerful features is Eden Treaty: a client library that provides end-to-end type safety between your backend and frontend. Serving TypeScript definitions efficiently across different environments requires some care. This post covers a production-ready approach to serving .d.ts files that works in both development and cloud deployments.

The Challenge: Type Definitions Across Environments

Eden Treaty relies on TypeScript definition files (.d.ts) to provide type inference for your API endpoints. Generating these types on-the-fly works well in development but creates problems in production:

  • Development: you want instant feedback as you modify your API.
  • Production: you want fast, predictable responses without runtime type generation overhead.

The Two-Mode Solution

The implementation uses two modes:

  1. Cloud Run (Production): serve pre-generated static type definitions from dist/types.d.ts.
  2. Development: generate types on-the-fly using elysia-remote-dts.

Implementation

The Elysia Configuration

Here's the core implementation that handles both cases:

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()
        // ... your middleware and routes
        .use(routes);

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

    // Serve TypeScript definitions for Eden Treaty
    // In Cloud Run: serve pre-generated static file from dist/types.d.ts
    // In development: generate on-the-fly using elysia-remote-dts
    // K_SERVICE is automatically set by 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;
}

Key Design Decisions

1. Environment Detection with K_SERVICE

Google Cloud Run automatically sets the K_SERVICE environment variable with the service name. This is a reliable way to detect production without manually setting NODE_ENV:

const isCloudRun = !!process.env.K_SERVICE;

2. Static File Existence Check

Before attempting to serve static types, we verify the file exists:

const hasStaticTypes = existsSync(staticTypesPath);

This prevents a crash if the static file wasn't generated for any reason. The system falls back to on-the-fly generation automatically.

3. Content-Type Header

When serving the .d.ts file, we set the appropriate content type:

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

This ensures browsers and tools correctly interpret the response as TypeScript.

Docker Build-Time Type Generation

Type generation happens once during the Docker build, not on every request:

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

COPY . .

RUN bun install

# Generate types.d.ts at build time (instead of on-the-fly)
RUN bun run generate-types

# Set timezone
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" ]

The Generate Types Script

Add a script to your package.json:

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

And the corresponding script:

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

// Generate TypeScript definitions for 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');

Benefits of This Approach

Zero Runtime Overhead in Production

Type generation happens once at build time. Response time is consistent, memory usage is lower (no type generation engine loaded), and there's no AST parsing per request.

Instant Updates in Development

elysia-remote-dts generates types on-the-fly during development. No build step required: just start the server and types update as you change your API.

Graceful Fallback

The conditional logic handles edge cases cleanly. If static types weren't generated, the system falls back to on-the-fly generation. This also covers local development without Cloud Run environment variables and staging environments.

Using Eden Treaty on the Client

With types being served, your frontend can consume them:

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

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

// Full type safety!
const { data, error } = await api.users.get();
//     ^? User[]

For dynamic type fetching, configure your IDE or build tools to fetch types from the /types.d.ts endpoint.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        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                          │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Build Pipeline

┌─────────────────────────────────────────────────────────────────────┐
│                     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                             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Performance Comparison

Metric On-the-fly Generation Static File Serving
Response Time 50-200ms 1-5ms
Memory Usage Higher (type engine) Minimal
CPU Usage Per-request parsing None
Cold Start Impact Significant Negligible

Best Practices

1. Version Your Types

Include a version comment in generated types:

// 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`;
// ... generate and prepend header

2. Cache Headers

Add caching headers for production:

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

3. CI/CD Integration

Verify types are generated correctly in your CI pipeline:

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

Conclusion

Serving TypeScript definitions for Eden Treaty comes down to one decision: generate at build time for production, generate on-the-fly for development. The dual-mode implementation handles this cleanly by reading the K_SERVICE environment variable that Cloud Run sets automatically.

The result: development stays fast and accurate (types always reflect your current API), while production serves static files in 1 to 5ms instead of 50 to 200ms. Both environments maintain full type safety for API consumers.

The same pattern applies to any scenario where you need environment-specific behavior for serving generated artifacts: detect the environment reliably and have a clear fallback.


Technical Achievement: Environment-aware type serving that reduces response times by 95% in production while maintaining full development flexibility.

Key Technologies: Elysia, Bun, Eden Treaty, Google Cloud Run, Docker, TypeScript