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:

$ glyph gen openapi tasks.yaml --out src/ --client
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}")
}
$ glyph gen openapi tasks.yaml --out src/ --handlers
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:

schemas.ts
export const Task = z.object({
  id: z.number(),
  title: z.string(),
  done: z.boolean(),
})
$ glyph gen zod schemas.ts --out src/
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/zodgen 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.