Contract-first clients without codegen

The TypeScript client package uses the same Zod schemas as the server, but it does not need to import the server or wait for an SDK generation step.

OpenAPI is still the right language-agnostic contract for other ecosystems. But inside a TypeScript monorepo, there is a shorter path: put schemas in a module that both server and client can import, then define route handles from those schemas.

export const HelloPath = z.object({name: z.string().min(1)});
export const Greeting = z.object({greeting: z.string()});

const greet = routeGroup('/greet');
const hello = greet.get('/hello/{name}', {
  path: HelloPath,
  response: Greeting,
});

const client = createClient({baseURL: 'http://localhost:3000'});
const out = await hello.call(client, {path: {name: 'Ada'}});

The result is typed at compile time and validated at runtime. Change the schema, and TypeScript points at the call sites that no longer match. No generated files are checked in, and no client package has to boot the server to understand its contract.

The import boundary matters

The schema module should be boring and side-effect free. Export it from a subpath or separate package, not from a server entry point that creates an application. That keeps browser clients and tests cheap.

See the working example in examples/hello-client and the guide section on type-safe clients with no codegen.