API Client Generation
OpenAPI to TypeScript with Kubb code generation
Vezta uses an automated pipeline to keep the frontend and mobile API clients in sync with the backend. The backend exports an OpenAPI spec, and Kubb generates TypeScript types, Zod schemas, fetch clients, and TanStack Query hooks from it.
The Pipeline
Backend (NestJS + Swagger)
│
▼
OpenAPI spec (openapi.json)
│
▼
Kubb (pnpm generate in vezta-fe)
│
▼
lib/api/gen/
├── models/ TypeScript interfaces
├── zod/ Zod validation schemas
├── client/ Typed fetch functions
└── hooks/ TanStack Query v5 hooks
│
▼
Copy to vezta-mobile/lib/api/gen/ (manual)Step 1: Export the OpenAPI Spec
The backend auto-exports openapi.json to its project root on every startup. You can also export it manually:
cd vezta-be && pnpm export:openapiThe spec is also available at http://localhost:3001/docs-json when the dev server is running. Swagger UI is at http://localhost:3001/docs.
Step 2: Generate the Frontend Client
With the backend running (or with a local openapi.json file):
cd vezta-fe && pnpm generateThis runs Kubb with the configuration in kubb.config.ts. The input source defaults to http://localhost:3001/docs-json but can be overridden with the OPENAPI_PATH environment variable.
Kubb Configuration
The kubb.config.ts file configures five plugins in sequence:
plugins: [
// 1. Parse and validate the OpenAPI spec
pluginOas({ validate: true }),
// 2. TypeScript interfaces (enums as const, dates as string)
pluginTs({ output: { path: "models" }, enumType: "asConst", dateType: "string" }),
// 3. Zod schemas for runtime validation
pluginZod({ output: { path: "zod" }, dateType: "string", inferred: true }),
// 4. Typed fetch client functions
pluginClient({ output: { path: "client" }, client: "fetch", dataReturnType: "data" }),
// 5. TanStack Query v5 hooks (grouped by API tag, suspense enabled)
pluginReactQuery({
output: { path: "hooks" },
client: { importPath: "@kubb/plugin-client/clients/fetch" },
group: { type: "tag" },
suspense: {},
}),
]The output goes to lib/api/gen/ with clean: true, meaning the entire directory is wiped and regenerated on each run.
Fetch Alias
Both webpack and turbopack alias @kubb/plugin-client/clients/fetch to lib/api/kubb-fetch.ts in next.config.ts. This means every generated hook automatically gets:
- Bearer token injection from the in-memory access token
- Automatic 401 refresh-and-retry logic
credentials: 'include'for cookie-based auth
No hand-editing of generated code is needed.
Step 3: Sync to Mobile
The mobile app's lib/api/gen/ directory is a direct copy of the frontend's generated code. After regenerating in vezta-fe, manually copy the output:
cp -r vezta-fe/lib/api/gen/* vezta-mobile/lib/api/gen/Never hand-edit files in lib/api/gen/ in any subproject. The directory is wiped and regenerated with clean: true on every pnpm generate run. Any manual changes will be lost.
When to Regenerate
Run pnpm generate in vezta-fe after any backend API change:
- New endpoint added to a controller
- DTO field added, removed, or renamed
- Response shape changed
- Swagger decorators updated (
@ApiProperty,@ApiOperation,@ApiResponse) - New controller or module registered
Kubb only generates offset-based pagination hooks. Cursor-based infinite queries (used for feeds, activity logs, etc.) must be wrapped manually in feature hooks using TanStack Query's useInfiniteQuery.
Troubleshooting
| Problem | Solution |
|---|---|
pnpm generate fails to connect | Ensure the backend is running on port 3001, or set OPENAPI_PATH to a local openapi.json file |
| Generated types are stale | Re-export the spec (pnpm export:openapi in vezta-be) and regenerate |
| Missing hooks for new endpoint | Verify the controller has @ApiTags and @ApiOperation decorators |
| Mobile app types out of sync | Copy vezta-fe/lib/api/gen/ to vezta-mobile/lib/api/gen/ |