API codegen
TypeScript clients are generated from the backend's OpenAPI spec via `@hey-api/openapi-ts`. `pnpm api:generate` writes to `packages/api-client/src/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 generated SDK lives in
@todayai-labs/api-client,
not in apps/web — every Today web client (apps/web, the upcoming
apps/tangible, future apps) imports it as a workspace dependency:
import { getV1Tasks, type V1Task } from '@todayai-labs/api-client'The script
pnpm api:generate # writes packages/api-client/src/generated/Behind the scenes (in packages/api-client/openapi-ts.config.ts):
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://api.todayai.dev/doc',
output: {
path: 'src/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',
],
})pnpm api:generate at the repo root forwards to pnpm --filter @todayai-labs/api-client generate, which is the canonical entry point.
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
packages/api-client/src/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
packages/api-client/src/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
packages/api-client/src/client-config.ts.
That file is not generated — it's the seam where the generated client
gets wired into the app's base URL via @todayai-labs/auth-client's
resolveApiBaseUrl():
// packages/api-client/src/client-config.ts
import { resolveApiBaseUrl } from '@todayai-labs/auth-client'
import type { CreateClientConfig } from './generated/client.gen'
export const createClientConfig: CreateClientConfig = (config) => ({
...config,
baseUrl: resolveApiBaseUrl(),
})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 the admin console?
The admin console lives in
todayai-labs/today-admin and
runs its own codegen against an admin-specific OpenAPI spec. The principle is
identical to the flow above; the spec source and output path differ. See
#749 for the
extraction history.
MSW + generated types
The MSW handlers and fixtures under
apps/web/src/lib/msw/
import the generated types from @todayai-labs/api-client 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
How web and Electron identify Linux, macOS, and Windows requests with X-Client-Platform and X-App-Version, including the Linux cloud rollout boundary.
Chat runtime
Cross-package boundary overview for the chat-v2 runtime in `apps/web`. Implementation details live next to the code under `apps/web/docs/chat/`.