This document explains how to generate type-safe API clients from OpenAPI specifications.
We use @hey-api/openapi-ts to generate TypeScript clients from OpenAPI specs provided by the Control Plane API. This ensures type safety between our frontend and the backend APIs.
Generated clients live in app/modules/control-plane/:
app/modules/control-plane/
├── authorization/
│ ├── sdk.gen.ts # API functions
│ ├── types.gen.ts # TypeScript types
│ └── schemas.gen.ts # Zod schemas
├── compute/
├── discovery/
├── dns-networking/
├── gateway/
├── iam/
├── identity/
├── k8s-core/
├── networking/
├── quota/
├── resource-manager/
├── telemetry/
├── shared/ # Shared utilities
│ ├── client/
│ └── core/
├── setup.client.ts # Client-side setup
└── setup.server.ts # Server-side setup
The easiest way to generate OpenAPI clients is using the interactive generator:
bun run openapiThis will:
- Prompt for API URL (defaults to
https://api.datum.netorAPI_URLenv var) - Prompt for Bearer Token (can be set via
API_TOKENenv var) - Fetch and display all available API resources
- Let you select which resources to generate
- Generate the TypeScript clients automatically
You can set these environment variables to skip the prompts:
# Set API URL
export API_URL=https://api.datum.net
# Set Bearer Token
export API_TOKEN=your-bearer-token-here
# Then run the generator
bun run openapiYou need a valid bearer token from your OIDC provider. Get it from:
- The browser DevTools (Network tab → Authorization header)
- Or by logging in and checking the session
The generator will show all available API resources. Common ones include:
| API Group | Description |
|---|---|
identity.miloapis.com |
User profile, sessions |
iam.miloapis.com |
Organizations, members, roles |
resourcemanager.miloapis.com |
Projects, resource management |
dns.networking.miloapis.com |
DNS zones, records, domains |
networking.miloapis.com |
HTTP proxies, networking |
compute.miloapis.com |
Compute resources |
quota.miloapis.com |
Quota management |
authorization.miloapis.com |
Access reviews |
Each generated module contains:
| File | Purpose |
|---|---|
sdk.gen.ts |
API functions (e.g., getOrganization()) |
types.gen.ts |
TypeScript types for requests/responses |
schemas.gen.ts |
Zod schemas for runtime validation |
index.ts |
Re-exports |
// Import generated functions
import { getOrganizations, createOrganization } from '@/modules/control-plane/iam';
// List organizations
const response = await getOrganizations();
const orgs = response.data?.items ?? [];
// Create organization
const newOrg = await createOrganization({
body: {
metadata: { name: 'my-org' },
spec: { displayName: 'My Organization' },
},
});Configures Axios for server-side requests:
// Automatically imported in server/entry.ts
import '@/modules/control-plane/setup.server';Features:
- Base URL from
API_URLenv var - Auth token injection via AsyncLocalStorage
- Request ID correlation
- Error handling
Configures Axios for browser requests:
// Automatically imported in entry.client.tsx
import '@/modules/control-plane/setup.client';Features:
- Proxy through BFF (
/api/proxy) - Cookie-based authentication
- CSRF protection
When the Control Plane API changes:
-
Run the generator:
bun run openapi
-
Select the resources that need updating
-
Update adapters if response shape changed:
// app/resources/{resource}/{resource}.adapter.ts export function toResource(response: NewApiResponse): Resource { // Update transformation }
-
Run type check:
bun run typecheck
-
Test the changes:
bun run test:e2e
# Restart TypeScript server in VS Code
# Or run:
bun run typecheckThe spec may be outdated. Re-run the generator:
bun run openapiCheck that:
- Your bearer token is valid and not expired
- You have network access to the API URL
- The selected API resource is available
For advanced use cases, you can still use @hey-api/openapi-ts directly:
# Generate from a local spec file
bunx openapi-ts --input ./specs/api.json --output ./app/modules/control-plane/api-nameOr configure in openapi-ts.config.ts:
import { defineConfig, defaultPlugins } from '@hey-api/openapi-ts';
export default defineConfig({
input: './specs/gateway.json',
output: './app/modules/control-plane/gateway',
plugins: [
...defaultPlugins,
'@hey-api/schemas',
{
enums: 'javascript',
name: '@hey-api/typescript',
},
],
});- Domain Modules - How to use generated clients
- Data Flow - Request lifecycle
- Adding a New Resource