Client Generation
Ontogen generates TypeScript client code that mirrors your server API surface. Three client generators cover different deployment scenarios: a pure HTTP client, a split transport that switches between HTTP and Tauri IPC at runtime, and an admin registry that provides per-entity metadata for admin UIs.
Client generator variants
Section titled “Client generator variants”Client generators are configured through the generators field on ClientsConfig and run by gen_clients (or, more commonly, Pipeline::clients(...).build()):
use ontogen::clients::ClientGenerator;use ontogen::ClientsConfig;
let clients_config = ClientsConfig { // ... api_dir, state_type, naming, etc. generators: vec![ ClientGenerator::HttpTs { output: "../src-nuxt/app/types/httpCommands.ts".into(), bindings_path: "../src-nuxt/app/types/bindings.ts".into(), }, ClientGenerator::HttpTauriIpcSplit { output: "../src-nuxt/app/transport/generated.ts".into(), bindings_path: "../src-nuxt/app/types/bindings.ts".into(), }, ClientGenerator::AdminRegistry { output: "../src-nuxt/layers/admin/generated/admin-registry.ts".into(), }, ], // ...};ClientGenerator lives at ontogen::clients::ClientGenerator (re-exported at the module level). The internal path ontogen::clients::config::ClientGenerator is pub(crate) and no longer accessible to downstream crates.
TypeScript HTTP client
Section titled “TypeScript HTTP client”The HttpTs generator produces a typed, fetch-based HTTP client. For each API function, it generates a TypeScript function that makes the appropriate HTTP request.
// Auto-generated HTTP client. DO NOT EDIT.
import type { Task, CreateTaskInput, UpdateTaskInput,} from './bindings';
const BASE = '/api';
async function httpGet<T>(path: string): Promise<T> { const res = await fetch(`${BASE}${path}`); if (!res.ok) throw new Error(await res.text()); return res.json();}
async function httpPost<T>(path: string, body: unknown): Promise<T> { const res = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); return res.json();}
// ... httpPut, httpDelete helpers
export async function taskList(): Promise<Task[]> { return httpGet('/tasks');}
export async function taskGetById(id: string): Promise<Task> { return httpGet(`/tasks/${id}`);}
export async function taskCreate(input: CreateTaskInput): Promise<Task> { return httpPost('/tasks', input);}
export async function taskUpdate(id: string, input: UpdateTaskInput): Promise<Task> { return httpPut(`/tasks/${id}`, input);}
export async function taskDelete(id: string): Promise<null> { return httpDelete(`/tasks/${id}`);}Function names follow the same entity-first convention as the server transports: taskList, taskGetById, taskCreate. The naming is camelCase for TypeScript conventions.
Custom endpoints discovered through API scanning get client functions too:
export async function taskGetOverdue(): Promise<Task[]> { return httpGet('/tasks/overdue');}The bindings_path option
Section titled “The bindings_path option”The bindings_path points to the TypeScript file that holds the type definitions the client imports. Ontogen owns this file — on every build it rewrites the path with a fresh set of TS aliases. Two emitters cooperate to populate it:
- Schema-known emitter — entity types from
src/schema/plus theCreate{Entity}Input/Update{Entity}InputDTOs Ontogen derives are emitted directly fromEntityDefvia a bounded mapping overFieldType. No user setup required. - Long-tail emitter — user-owned types referenced by a custom API endpoint’s input or return type (anything that isn’t an entity or a generated DTO) are emitted by the
ontogen-tscrate. A build-time AST walker scans yoursrc/for the reachable closure of every long-tail type and appends the result tobindings_path.
The long-tail emitter runs entirely inside build.rs. There is no separate compilation, no extra binary, no cargo invocation, no CARGO_TARGET_DIR isolation. The walker reads .rs files with syn and emits TypeScript directly from the AST.
You don’t need to opt long-tail types in — the walker discovers them automatically from your API signatures and recursively pulls in everything they reference. If a referenced type lives in a workspace-sibling crate, declare the sibling’s source root in pool_extra_roots; see the TypeScript bindings guide.
If a type’s shape isn’t in the supported subset, or a serde attribute isn’t recognized, or a reference can’t be resolved, the build hard-errors with the full punch-list. See the error model for the four variants and how to handle each.
For the full ontogen-ts story — supported subset, serde rename family, BigInt rendering, external types, the #[ontogen::ts_opaque] / #[ontogen::ts_name] escape hatches — see the dedicated TypeScript bindings guide.
Split transport (HTTP + Tauri IPC)
Section titled “Split transport (HTTP + Tauri IPC)”The HttpTauriIpcSplit generator is for applications that run as both a web app and a Tauri desktop app. It generates a Transport interface with two implementations:
interface Transport { taskList(): Promise<Task[]>; taskGetById(id: string): Promise<Task>; taskCreate(input: CreateTaskInput): Promise<Task>; // ... all operations}
function createHttpTransport(): Transport { return { taskList: () => httpGet('/tasks'), taskGetById: (id) => httpGet(`/tasks/${id}`), taskCreate: (input) => httpPost('/tasks', input), // ... };}
function createIpcTransport(): Transport { return { taskList: () => invoke('task_list'), taskGetById: (id) => invoke('task_get_by_id', { id }), taskCreate: (input) => invoke('task_create', { input }), // ... };}At runtime, you detect the environment and pick the right transport:
const transport = window.__TAURI__ ? createIpcTransport() : createHttpTransport();Both implementations have identical signatures. Code that uses the transport doesn’t know or care whether it’s talking to a local server over HTTP or to the Tauri backend over IPC.
Route prefix in client code
Section titled “Route prefix in client code”When route_prefix is configured, the generated client functions accept an optional project ID parameter:
export async function taskList(projectId?: string): Promise<Task[]> { const prefix = projectId ? `/projects/${projectId}` : ''; return httpGet(`${prefix}/tasks`);}For the IPC transport, the project ID is passed as an additional argument to invoke:
taskList: (projectId) => invoke('task_list', { projectId: projectId ?? null}),Admin registry
Section titled “Admin registry”The AdminRegistry generator produces a TypeScript file with per-entity metadata for admin UIs. This is designed for generic admin interfaces that can render CRUD forms and tables without hand-written configuration.
// Auto-generated admin registry. DO NOT EDIT.
import type { AdminFieldDef, AdminEntityConfig } from '@ontogen/admin-types'
export const adminEntities: AdminEntityConfig[] = [ { key: 'task', plural: 'tasks', label: 'Task', pluralLabel: 'Tasks', idType: 'string', listMethod: 'taskList', getMethod: 'taskGetById', createMethod: 'taskCreate', updateMethod: 'taskUpdate', deleteMethod: 'taskDelete', returnType: 'Task', createInputType: 'CreateTaskInput', updateInputType: 'UpdateTaskInput', paginated: false, fields: [ { key: 'name', label: 'Name', type: 'string', role: 'plain', required: true }, { key: 'status', label: 'Status', type: 'string', role: 'enum', required: false }, { key: 'assignee_id', label: 'Assignee', type: 'string', role: 'relation', relation: { kind: 'belongs_to', target: 'agent' }, required: false }, ], }, // ... more entities];The registry only includes modules that have all five CRUD functions (list, get_by_id, create, update, delete). Partial or custom-only modules are excluded.
Per-field metadata
Section titled “Per-field metadata”When schema_entities is populated on ClientsConfig (the Pipeline builder forwards this automatically from the schema stage), the admin generator emits detailed field information:
- type — the TypeScript type (
string,number,boolean, arrays) - role — the field’s purpose (
id,plain,enum,relation,body) - relation metadata — for relation fields, the kind (
belongs_to,has_many,many_to_many) and target entity - required — whether the field is required vs optional
This lets admin UI frameworks render appropriate input components (text fields, dropdowns, relation pickers) without per-entity configuration.
Pagination metadata
Section titled “Pagination metadata”When pagination is configured, each entity config includes pagination details:
{ paginated: true, defaultLimit: 50, maxLimit: 200, // ...}Admin UIs can use this to render pagination controls with sensible defaults.
ts_skip_commands
Section titled “ts_skip_commands”Some IPC commands shouldn’t appear in the TypeScript client — perhaps they’re internal or handled through a different mechanism. The ts_skip_commands list on ClientsConfig excludes specific commands by their canonical name:
ts_skip_commands: vec![ "system_health_check".to_string(), "internal_reindex".to_string(),],Skipped commands are omitted from both the HTTP client and the split transport. They still get server-side handlers (those are controlled by ServersConfig, which has its own dispatch).
How client functions mirror the server API
Section titled “How client functions mirror the server API”The client generators discover functions through the same ApiModule list that the server generators use. For each function, the generator:
- Determines the HTTP method and path from
OpKind. - Maps Rust parameter types to TypeScript types using the
rust_type_to_tsfunction. - Checks which TypeScript types are available in
bindings.ts. - Generates a function with matching name, parameters, and return type.
This means custom API endpoints you add through scan_dirs automatically get client functions too. You write the Rust function, rebuild, and the TypeScript client has a new typed function ready to call.