Skip to content

Generate a typed client

@grundlag/client gives an application end-to-end type safety against a specific host: the exact providers it serves, with the exact input and output shapes of their actions and entities. It does this by reading the host’s manifest — its self-description — and compiling it into TypeScript. This guide covers that generation step; the two guides after it use what it produces.

A host publishes a manifest at GET /api/client/schema: for every provider, its actions, entities, and events, each with metadata, JSON Schemas, and (for actions) the function name the server registered it under. generateClientTypes fetches that manifest and compiles it into one source file that exports two things:

Export Kind Used by
ApiSchema a compile-time type createClient<ApiSchema> — the typed proxy client
apiManifest a runtime const client.toTools(apiManifest) — the runtime tool set

ApiSchema is pure compile-time contract — nothing of it exists at runtime. apiManifest is the runtime metadata a sandbox or agent needs to reconstruct callable tools. Both come from the same manifest, so they never drift.

Generation is a step you run against a live server and commit the output of — the same shape as generating types from an OpenAPI document or a database schema. Point it at a running host, fetch, compile, write:

import { writeFile } from 'node:fs/promises';
import { generateClientTypes } from '@grundlag/client';
const source = await generateClientTypes({ url: 'http://localhost:3800' });
await writeFile('./src/client.generated.ts', source);

Run this whenever the host’s providers change, and check the generated file into your repo so consumers get the current types without a server running.

If you own the server, you don’t need a separately running process — boot it in the script on an ephemeral port, generate, and shut it down. This keeps the types generated from exactly the providers your code registers:

import { writeFile } from 'node:fs/promises';
import { generateClientTypes } from '@grundlag/client';
import { createMyServer } from './server.js';
const server = await createMyServer();
await server.listen({ port: 0, host: '127.0.0.1' });
try {
const address = server.server.address();
if (!address || typeof address === 'string') {
throw new Error('Expected the server to be listening on a TCP port');
}
const source = await generateClientTypes({ url: `http://127.0.0.1:${address.port}` });
await writeFile('./src/client.generated.ts', source);
} finally {
await server.close();
}

generateClientTypes takes the manifest source plus a couple of output controls:

Option Default Description
url Base URL of the host, e.g. http://localhost:3800. No trailing slash.
prefix /api Route prefix the host mounts its routes under.
fetch global fetch A fetch implementation to use.
headers Headers to send with the manifest request — e.g. an auth token if the host is protected.
typeName ApiSchema Name of the generated root type.
bannerComment a default Banner prepended to the output.

If you only need the raw manifest — for example to build tools without generating a type file — fetchClientManifest returns it directly with the same source options.