API codegen
TypeScript clients are generated from the backend's OpenAPI spec via `@hey-api/openapi-ts`. `pnpm api:generate` writes to `apps/web/src/lib/api/generated/` — never edit those files by hand.
The cloud backend exposes an OpenAPI spec at
https://api.todayai.dev/doc. The web client side consumes it through a
code generator: TypeScript types, request/response Zod schemas, and a
fetch-based SDK, all generated and committed.
The script
pnpm api:generate # writes apps/web/src/lib/api/generated/Behind the scenes (in apps/web/openapi-ts.config.ts):
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://api.todayai.dev/doc',
output: {
path: 'src/lib/api/generated',
},
plugins: [
'@hey-api/typescript',
{
name: '@hey-api/client-fetch',
runtimeConfigPath: '../client-config.ts',
},
{
name: 'zod',
definitions: true,
requests: true,
responses: true,
},
'@hey-api/sdk',
],
})What gets generated
| Plugin | Output |
|---|---|
@hey-api/typescript | types.gen.ts — TS types for every schema in the OpenAPI spec |
@hey-api/client-fetch | client.gen.ts — fetch-based client wired to client-config.ts |
zod | zod.gen.ts — Zod schemas matching requests + responses + definitions |
@hey-api/sdk | sdk.gen.ts — typed SDK methods, one per OpenAPI operation |
The output lives under
apps/web/src/lib/api/generated/
and is committed. Never edit these files by hand — they're regenerated
on every pnpm api:generate run, and your changes will be silently
overwritten on the next regeneration. oxlint.config.ts includes
**/lib/api/generated/ in its ignore patterns so we don't fight the
generator over style.
The runtime config bridge
The client-fetch plugin's runtimeConfigPath: '../client-config.ts'
points at a hand-written file at
apps/web/src/lib/api/client-config.ts.
That file is not generated — it's the seam where the generated client
gets wired into the app's auth + headers + base URL:
// apps/web/src/lib/api/client-config.ts (sketch)
import { client } from './generated/client.gen'
import { CLIENT_PLATFORM_HEADER_VALUE, APP_VERSION_HEADER_VALUE } from './platform-constants'
client.setConfig({
baseURL: resolveApiBaseUrl(),
headers: {
'X-Client-Platform': CLIENT_PLATFORM_HEADER_VALUE,
'X-App-Version': APP_VERSION_HEADER_VALUE,
},
})Hand-written + regeneration-safe = the split that makes this codegen flow viable. The generated code never embeds the base URL or auth-relevant headers; those are wired through the runtime config.
When to regenerate
| Trigger | Action |
|---|---|
| Backend ships a new OpenAPI operation | pnpm api:generate + commit |
| Backend tightens an existing schema | pnpm api:generate + fix any consumers that broke |
| Local dev wants to point at a different spec source | Edit input: in openapi-ts.config.ts (don't commit), regenerate, then revert |
What about apps/admin?
apps/admin does not consume the same generated client. It has its own
admin-specific API endpoints and codegen flow (under
apps/admin/src/lib/api/). The principle is identical; the spec source and
output path differ.
MSW + generated types
The MSW handlers and fixtures under
apps/web/src/lib/msw/
import the generated types so mock fixtures stay shaped like real API
responses. The
fixtures-contract.test.ts
test runs the generated Zod schemas against every fixture — when the
schema changes after a regeneration, the test points at the field that
diverges. This is the safety net that keeps mock-mode honest.
See MSW mocks for the mock layer's role in the architecture and Toolchain → Testing → MSW for the implementation layout.
Common issues
pnpm api:generatefetches nothing. The OpenAPI URL is the preview-environment spec (api.todayai.dev/doc). If that environment is down or behind protection, regeneration fails. Use a local mirror or pointinput:at a saved spec file temporarily.- Generated types disagree with what the backend actually sends. This happens when the spec is out of sync with the deployed backend — the deployed code is ahead of (or behind) the spec generator. Reach out to the cloud team to confirm what's authoritative.
fixtures-contract.test.tsfails after regeneration. A fixture shape drifted from the new schema. Update the fixture to match — that's the contract test working as intended.
Related
- Cross-platform headers — what
client-config.tsinjects into every request - MSW mocks — how generated types make mock fixtures honest
- Toolchain → Testing → MSW — handler / fixture implementation
Cross-platform headers
X-Client-Platform and X-App-Version are sent on every API request from web, macOS, and Android. The cloud server treats web-client as a first-class platform — no special-casing needed.
Chat runtime
A high-level overview of the chat-v2 runtime — Postbox-style store, SSE-driven live source, paginated history, holes — with pointers to the full local docs under `apps/web/docs/chat/`.