06 · From your spec
Can I generate a typed client and server from my API spec?
Yes. Point glyph gen openapi at an OpenAPI spec: --client
emits a typed HTTP client, one function per operation; --handlers emits
server stubs and a working route dispatcher. If your source of truth is a
zod schema, glyph gen zod does the same. Everything comes out as
committed, greppable Glyph you own and edit — never a hidden
codegen step.
Why it matters
Hand-written API clients and their DTOs drift from the spec, and the drift is a class of
bug nobody enjoys. Because Glyph generates real code — typed functions,
real type declarations with runtime descriptors — the spec stays the
source of truth, and regeneration is an idempotent, reviewable diff, not a black box.
See it — the client
Each operation becomes an async fn with typed path parameters and a typed
request body, returning the HTTP Response:
async fn createTask(
base: string,
body: NewTask,
) -> Result<Response, HttpError> {
return await post(
"${base}/tasks",
body,
)
}
async fn getTask(base: string, id: number)
-> Result<Response, HttpError> {
return await get("${base}/tasks/${id}")
}
fn route(req: Request)
-> Result<Response, string> {
return match req.method {
"GET" => match segments(req) {
["tasks"] => listTasks(req),
// id captured from the path:
["tasks", id] => getTask(req, id),
else => Ok(json(404, {
error: "not found",
})),
},
else => Ok(json(405, {
error: "method not allowed",
})),
}
}
The --handlers router matches the path with array patterns, so
/tasks/{id} becomes ["tasks", id] and binds the parameter for
you. Each stub starts as a 501 with a comment showing the body-parse; you
fill them in and wire it up with await serve(PORT, route).
From a zod schema instead
If your team already defines shapes with zod, generate from those directly
— the schema becomes a first-class Glyph type with its own descriptor:
export const Task = z.object({
id: z.number(),
title: z.string(),
done: z.boolean(),
})
type Task = {
id: number,
title: string,
done: bool,
}
// + a runtime descriptor, so
// Task.parse(value) validates.
A polymorphic discriminated union (an OpenAPI discriminator)
comes out as a real Glyph tagged union plus a generated parse_<Name>
that reads the discriminator property and validates into the right variant — because
Glyph tags by constructor name, not an arbitrary property, the generated dispatcher bridges
the wire object to the union for you.
The honest edge: generation is wire-faithful, so it maps the common 80%
— objects, primitives, arrays, references, optional and nullable fields, and
discriminated unions — and narrows what it can't represent exactly (a string enum, an
undiscriminated union) with a printed note rather than a validator that would
reject real data. Richer enums are next. And the tooling has real dependencies: gen dts/gen
zod need Node plus typescript/zod — gen
dts works with any TypeScript (5, 6, or the 7.x native port), resolved from your
project first — and they tell you exactly what to install if it's missing.
Keeping it in sync
Every generated file records the exact command that made it in its header. When a spec
changes, you don't hunt for which commands to re-run — glyph regen scans
for those headers and re-runs each one, rewriting the output. It's idempotent, so a regen
with no spec change touches nothing; it drops cleanly into a pre-commit hook or a CI check
that fails if the committed code has drifted from the spec.
Where it stands
Shipping today
glyph gen openapi (types, --client, --handlers with a real router), glyph gen dts, glyph gen zod, and glyph regen to refresh it all from source — every command emitting committed, descriptor-bearing, idempotently-regenerated Glyph.
On the way
Richer enum mapping and broader OpenAPI coverage — each landing behind the same wire-faithful, note-when-narrowed rule.