Pie
@dex/pie (Dex Pie)
A high-performance, framework-agnostic typed API client and OpenAPI route-tree generator:
- Turns any backend API contract (Phoenix, Go, Rails, FastAPI, Express) into a typed, autocompleteable route tree (
api.v1.guilds('123').channels('456').messages.get(...)). - Uses OpenAPI as the cross-repository bridge to generate route trees and DTO interfaces into
core/api/generated.ts. - Built-in resilient transport: exponential backoff with jitter retries, dynamic auth headers, request/response interceptors, and one-off
baseUrloverrides. - First-party Eden Treaty adapter (
treatyPie) available for fullstack Elysia setups.
1. Generating Route Tree from Backend OpenAPI
Run the dex pie generate CLI command pointing to your backend's OpenAPI URL or local JSON file:
bunx dex pie generate https://api.concord.chat/api/openapi.jsonOr configure dex.config.ts:
// dex.config.ts
export default {
mode: 'spa',
apiSpec: 'https://api.concord.chat/api/openapi.json',
apiUrl: 'https://api.concord.chat',
outApiTs: 'core/api/generated.ts',
apiPrefix: '/api', // Stripped from tree access so you write api.v1... not api.api.v1...
}Then simply run:
bunx dex pie generateThis generates core/api/generated.ts containing:
- All schema models & DTO interfaces (e.g.
Message,User,CreateMessageDto). - The
ApiRoutesroute tree contract. - The preconfigured
createApiClient()factory function.
2. Consuming the Typed API Route Tree in Frontend
// core/api/index.ts
import { createApiClient } from './generated'
export const api = createApiClient({
headers: () => ({
Authorization: `Bearer ${getAuthToken()}`,
}),
retry: {
retries: 3,
minDelayMs: 150,
maxDelayMs: 2000,
retryOnStatuses: [408, 425, 429, 500, 502, 503, 504],
},
})Making Calls:
// 1. Static route with query parameters:
// GET /api/v1/health?verbose=true
const { data, error, status } = await api.v1.health.get({
query: { verbose: true }
})
// 2. Dynamic subroutes:
// GET /api/v1/guilds/123/channels/456/messages?limit=50
const { data: messages } = await api.v1
.guilds('123')
.channels('456')
.messages.get({
query: { limit: 50 }
})
// 3. Typed request body:
// POST /api/v1/channels/456/messages
const { data: createdMessage } = await api.v1
.channels('456')
.messages.post({
body: {
content: 'Hello Concord!',
}
})
// 4. One-off base URL override (e.g. calling a dedicated voice microservice):
const { data: voiceSession } = await api.voice.session.get({
baseUrl: 'https://voice.concord.chat'
})3. First-Party Elysia Eden Mode (Optional)
If backend and frontend share TypeScript types in a monorepo with Elysia:
import { treatyPie } from '@dex/pie/treaty'
import type { App } from './server'
export const elysiaClient = treatyPie<App>('http://localhost:3000')