Codegen
Zwaggen specs are the single source of truth for your API contract. Codegen turns a .zwag spec into TypeScript types, Zod runtime validators, and a typed client your frontend can call directly — no more hand-typing interfaces from Swagger by eye, no manual sync ritual when the contract changes.
If you want to see the generated code update as you edit (without running --watch in a terminal), the web app has a Live preview panel in the top-right toolbar (look for the right-panel icon). It shows TS / Zod / Client / OpenAPI tabs that re-render 300ms after every spec change.
Install
The zwag CLI ships with codegen built in. Install via npm/pnpm/bun:
pnpm add -D @zwaggen/cli zod
# or
npm install -D @zwaggen/cli zod
# or
bun add -d @zwaggen/cli zodzod is a peer dependency — the generated schemas.ts imports from it.
Quick start
Given a spec at ./api.zwag (the file you save from Zwaggen Web):
zwag generate ts ./api.zwag --out ./src/generated --clientThis writes three files into ./src/generated/:
types.ts— pure TypeScript interfaces and type aliases for every Type in the specschemas.ts— Zod schemas (UserSchema,AdminSchema, …) for runtime validationclient.ts— a typed client object you instantiate against your API base URL
Using the generated client
import { createClient } from './generated/client';
const api = createClient({
baseUrl: 'https://api.example.com',
headers: () => ({ authorization: `Bearer ${getToken()}` }),
});
const users = await api.users.listUsers(); // typed: Promise<User[]>
const me = await api.users.getUser({ id: 'u1' });
const made = await api.users.createUser({ body: { id: 'u9', name: 'New' } });Every response is validated through its Zod schema — if the API drifts and returns the wrong shape, you get a clear runtime error at the call site instead of a mysterious undefined deeper in your component tree.
Watch mode
During development, run codegen in the background. It debounces and re-runs on every save:
zwag generate ts ./api.zwag --out ./src/generated --client --watchAdd it to your dev script:
{
"scripts": {
"codegen": "zwag generate ts ./api.zwag --out ./src/generated --client",
"codegen:watch": "zwag generate ts ./api.zwag --out ./src/generated --client --watch",
"dev": "concurrently 'pnpm codegen:watch' 'vite'"
}
}Schemas-only mode
If you only want runtime validators (e.g., your TS types come from somewhere else):
zwag generate zod ./api.zwag --out ./src/generatedThis emits only schemas.ts.
Universal runtime
Generated code uses only globalThis.fetch, URL, JSON, and zod. It runs unchanged in:
- Browsers (modern evergreen)
- Node.js ≥ 18
- Deno
- Bun
- React Native (with a fetch polyfill if your target is below RN 0.71)
There are no Node-specific imports in the generated output. Your bundler (Vite, esbuild, webpack, Rollup, …) handles the rest.
Should I commit the generated files?
Two patterns work; pick the one that fits your team:
Pattern A — commit them. PRs show meaningful diffs when the contract changes. CI just runs tsc. Reviewers see exactly what changed type-wise.
Pattern B — .gitignore them and regenerate in CI. Keeps the repo lean. Add pnpm codegen to your CI's prebuild step. The trade-off: PRs don't show the type changes inline, and you need a clean spec → code pipeline.
Pattern A is more common for product teams; pattern B is more common for libraries.
Determinism
Generated output is deterministic — same spec → byte-identical files across machines, OSes, and runs. Safe to commit, safe to compare in CI, safe to review in PRs.
Output flags
zwag generate ts <spec> [options]
--out <dir> Output directory (default: ./zwaggen-generated)
--client Also emit client.ts (typed client object)
--no-types Skip types.ts
--no-schemas Skip schemas.ts
--watch Re-run on spec change (200ms debounce)
zwag generate zod <spec> [options]
--out <dir> Output directory (default: ./zwaggen-generated)
--watch Re-run on spec changeWhat about backend?
Backend frameworks already eat OpenAPI. Use zwag to export an OpenAPI 3 document from your spec, then point your existing backend codegen (NestJS, FastAPI, openapi-generator, oazapfts, …) at it. The contract stays the same; backend and frontend converge from the same .zwag file.
v1.1 additions
The v1.1 + v1.2 follow-ups shipped fixes for the original v1 limitations:
- Folder-prefixed type keys are sanitized. Types under a folder (key like
auth/User) emit valid identifiers (auth_Userin TS,auth_UserSchemain Zod). Inheritance, refs, and inline-object expressions all use the sanitized form symmetrically. - Inline object types in endpoint inputs. When an endpoint declares its
requestBodyor a path/query param inline (without a named ref), the generator now expands the structure into the client method signature instead of falling back tounknown. Both the TS type and the Zod schema mirror the inline shape. - Async
headersfactories.opts.headersacceptsRecord<string, string>,() => Record<string, string>, or() => Promise<Record<string, string>>. Async token refresh works directly — the client awaits the factory before sending each request. - Tag-grouped client API uses camelCase. Endpoints with OpenAPI
tags[]are grouped on the client (client.users.get(...)instead of a flat method list); group keys are camelized so multi-word tags like"User Profiles"becomeclient.userProfiles.
v1 limitations
- Path/query/header collisions. The input shape is a flat intersection. If the same name appears as both a path param and a query param, the resulting TS type is contradictory (TS will complain). Rename one of them in the spec.
Troubleshooting
"Cannot find module 'zod'" — pnpm add -D zod (or your package manager equivalent) in the project that consumes the generated code.
"Property X does not exist on type Y" — your spec doesn't define field X. Open the spec, add the field, regenerate.
Tests pass but the live API returns the wrong shape — that's drift. The generated Zod parse will throw with a precise path to the mismatched field. Fix the API or update the spec; either way the contract was wrong.