Skip to content

OpenAPI Workflow ​

TT Time Tracker maintains end-to-end type safety between the NestJS API and the Vue frontend through an OpenAPI-driven code generation pipeline.

The pipeline ​

NestJS controllers + DTOs
        │
        │ (Swagger decorators)
        ▼
  OpenAPI spec (JSON)
        │
        │ pnpm openapi
        ▼
  src/api/sdk.gen.ts
  (typed functions, one per endpoint)
        │
        │ import { Entries } from '@/api/sdk.gen'
        ▼
  Vue components

Step 1 — Annotate with Swagger ​

NestJS generates the OpenAPI spec from decorators in controllers and DTOs:

typescript
@ApiTags('Entries')
@Controller('tenants/:tenantId/entries')
export class EntryController {
  @Get()
  @ApiOkResponse({ type: [EntryDto] })
  findAll(): Promise<EntryDto[]> { ... }
}

The spec is served at GET /api/docs-json by the running API, but the codegen pipeline doesn't hit that URL — it exports the spec to a file first (see Step 2).

The plugin must run under the Nest CLI

DTO schemas are populated by the @nestjs/swagger transformer plugin (services/api/nest-cli.json), which only runs under the Nest CLI. The API's dev, start:debug, build, and openapi:export scripts therefore all go through nest. Exporting from a tsx- or plain-tsc-built API silently drops every request-body DTO (body?: never).

Step 2 — Export the spec ​

pnpm openapi runs this automatically, but you can run it on its own:

bash
pnpm --filter @tt/api openapi:export
# nest start with EXPORT_SWAGGER=openapi.json → writes services/api/openapi.json, then exits

services/api/openapi.json is a gitignored build artifact, regenerated on demand — not committed. The export boots the app (so it needs the same DB/Redis a normal boot needs), builds the Swagger document, writes the file, and exits.

Step 3 — Generate the client ​

bash
pnpm openapi
# services/api openapi:export → then openapi-ts reads services/api/openapi.json → writes services/client/src/api/

The generator (@hey-api/openapi-ts) produces one namespace per Swagger tag, with fully typed request and response shapes.

Why not tRPC or GraphQL? ​

tRPC requires TypeScript on both ends and tight coupling between the server and client through a shared type package. This works well for monorepos but adds complexity to the build pipeline and makes it harder to call the API from non-TypeScript clients.

GraphQL solves a different problem — flexible querying of complex graphs. TT Time Tracker's API is a straightforward REST CRUD surface. GraphQL's overhead (schema definition, resolvers, N+1 considerations) is not justified here.

OpenAPI gives us:

  • Standard HTTP semantics (cacheable GETs, idempotent PUTs)
  • A Swagger UI for easy exploration and manual testing
  • The ability to call the API from any HTTP client (including external integrations via API keys)
  • Type-safe generated clients without coupling the frontend and backend build pipelines

The main downside is that regeneration is a manual step: after changing the API you must run pnpm openapi (which exports the spec and regenerates in one go) to refresh the frontend SDK. This is a low friction cost in practice.

TT Time Tracker — Internal Documentation