fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: update NestJS research document with implementation plan and type-safe controller interface details.

+589 -30
+545
docs/implementation-plan-nestjs-plugin.md
··· 1 + # NestJS Plugin Implementation Plan 2 + 3 + > Consensus output from 4-agent architecture debate. Synthesized from competing proposals by Architecture Lead (minimal MVP), Feature Architect (fuller implementation), DX Engineer (developer experience), and Devil's Advocate (stress-testing). 4 + 5 + ## Executive Summary 6 + 7 + Generate type-safe controller interfaces from OpenAPI specs for NestJS applications. The plugin generates a **flat `ControllerMethods` interface** mapping operation IDs to typed method signatures. NestJS developers add `implements Pick<ControllerMethods, ...>` to their controller classes for compile-time contract enforcement. 8 + 9 + **Why not per-tag grouping for MVP?** The Devil's Advocate and Architecture Lead both argued convincingly that tag-based grouping introduces edge cases (no tags, multiple tags, tag naming collisions) with marginal MVP benefit. The flat approach mirrors Fastify exactly, avoids grouping complexity, and `Pick<>` gives developers natural per-controller scoping. Per-tag grouping is a strong v1.1 feature once the flat baseline is proven. 10 + 11 + --- 12 + 13 + ## Debate Resolution Summary 14 + 15 + | Decision | Resolution | Winning Argument | 16 + | ---------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 17 + | Output format | Flat `ControllerMethods` interface | Arch Lead + Devil's Advocate: avoids all tag edge cases, mirrors Fastify, `Pick<>` is composable | 18 + | `type` vs `interface` | `interface` | DX Engineer: `implements` keyword works with interfaces, better IDE support | 19 + | Parameters | Individual params: `path`, `query`, `body`, `headers` | All agreed: maps 1:1 to NestJS `@Param()`, `@Query()`, `@Body()`, `@Headers()` | 20 + | Return type | `Promise<OperationResponse>` (union of success responses) | DX Engineer: controllers return values, not status-code maps | 21 + | Tag grouping | Deferred to v1.1 | Devil's Advocate: edge cases (no tags, multi-tags) add complexity with marginal MVP gain | 22 + | Decorator generation | Deferred to v2 | All agreed: not MVP scope | 23 + | Config options | None for MVP | Arch Lead: zero config = fastest path to value | 24 + | `implements` viability | Works with documented constraint | Devil's Advocate confirmed: TS decorators don't affect signature matching, but users must use whole-object param style | 25 + 26 + --- 27 + 28 + ## Critical Design Decision: The `implements` Constraint 29 + 30 + The Devil's Advocate identified the most important technical finding: 31 + 32 + **TypeScript `implements` works with NestJS decorated parameters**, but only if the user adopts whole-object parameter style: 33 + 34 + ```typescript 35 + // WORKS with implements - whole-object style (recommended) 36 + @Get(':petId') 37 + async showPetById(@Param() params: ShowPetByIdData['path']): Promise<ShowPetByIdResponse> { ... } 38 + 39 + // DOES NOT WORK with implements - decomposed style 40 + @Get(':petId') 41 + async showPetById(@Param('petId') petId: string): Promise<ShowPetByIdResponse> { ... } 42 + ``` 43 + 44 + The whole-object style is a recognized NestJS pattern and arguably cleaner. This constraint must be prominently documented. 45 + 46 + **Known limitations to document:** 47 + 48 + - Methods using `@Res()` for raw response access are incompatible (extra parameter breaks assignability) 49 + - File upload operations using `@UploadedFile()` may need to bypass `implements` for specific methods 50 + - SSE endpoints returning `Observable<MessageEvent>` don't match `Promise<T>` return type 51 + 52 + --- 53 + 54 + ## Exact Output Format 55 + 56 + ### Generated `nestjs.gen.ts` 57 + 58 + For the Petstore spec: 59 + 60 + ```typescript 61 + // This file is auto-generated by @hey-api/openapi-ts 62 + 63 + import type { 64 + CreatePetsData, 65 + CreatePetsResponse, 66 + ListPetsData, 67 + ListPetsResponse, 68 + ShowPetByIdData, 69 + ShowPetByIdResponse, 70 + } from "./types.gen"; 71 + 72 + export interface ControllerMethods { 73 + createPets(body: CreatePetsData["body"]): Promise<CreatePetsResponse>; 74 + listPets(query?: ListPetsData["query"]): Promise<ListPetsResponse>; 75 + showPetById(path: ShowPetByIdData["path"]): Promise<ShowPetByIdResponse>; 76 + } 77 + ``` 78 + 79 + ### Design Decisions in Output 80 + 81 + 1. **`interface` (not `type`)** -- enables `implements`, better IDE experience with "Implement Interface" quick-fixes 82 + 2. **Individual method parameters** -- `path`, `query`, `body`, `headers` in that order, matching NestJS decorator extraction 83 + 3. **Optional params use `?`** -- uses `hasParameterGroupObjectRequired()` (same as Fastify) to determine optionality 84 + 4. **Return type is `Promise<OperationResponse>`** -- the union of success response body types, not the status-code-indexed `Responses` map. NestJS controllers return values directly. 85 + 5. **No external imports** -- unlike Fastify which imports `RouteHandler` from the `fastify` package, NestJS output is pure TypeScript. Zero runtime dependencies. 86 + 6. **Methods sorted alphabetically** -- matches existing plugin conventions (eslint sort-keys) 87 + 88 + ### Parameter Mapping 89 + 90 + ``` 91 + OpenAPI → Method Parameter 92 + ───────────────────────────────────────────── 93 + operation.parameters.path → path: OperationData['path'] (required if any path param exists) 94 + operation.parameters.query → query?: OperationData['query'] (optional based on hasParameterGroupObjectRequired) 95 + operation.body → body: OperationData['body'] (required based on operation.body.required) 96 + operation.parameters.header→ headers?: OperationData['headers'] (optional based on hasParameterGroupObjectRequired) 97 + ``` 98 + 99 + Parameter order: `path`, `query`, `body`, `headers`. Required params before optional where possible. 100 + 101 + ### Response Type Mapping 102 + 103 + Use the existing `*Response` type alias generated by the TypeScript plugin (the union of success response bodies), NOT the `*Responses` status-code-indexed type. This matches how NestJS controllers work -- they return a value, not a status-code mapping. 104 + 105 + For operations with error responses, errors are excluded from the return type. NestJS handles errors via exception filters, not return types. 106 + 107 + For `204 No Content`, the return type is `Promise<void>`. 108 + 109 + --- 110 + 111 + ## Usage Patterns 112 + 113 + ### Basic Usage 114 + 115 + ```typescript 116 + import { Controller, Get, Post, Param, Query, Body } from "@nestjs/common"; 117 + import type { ControllerMethods } from "../client/nestjs.gen"; 118 + import type { 119 + ListPetsData, 120 + ShowPetByIdData, 121 + CreatePetsData, 122 + } from "../client/types.gen"; 123 + 124 + @Controller("pets") 125 + export class PetsController 126 + implements Pick<ControllerMethods, "listPets" | "createPets" | "showPetById"> 127 + { 128 + @Get() 129 + async listPets(@Query() query?: ListPetsData["query"]) { 130 + return []; 131 + } 132 + 133 + @Post() 134 + async createPets(@Body() body: CreatePetsData["body"]) { 135 + return { id: 1, name: body.name }; 136 + } 137 + 138 + @Get(":petId") 139 + async showPetById(@Param() path: ShowPetByIdData["path"]) { 140 + return { id: path.petId, name: "Kitty" }; 141 + } 142 + } 143 + ``` 144 + 145 + ### Incremental Adoption (DX Engineer's Key Insight) 146 + 147 + Unlike Fastify where you must provide ALL handlers at once, NestJS developers can adopt incrementally: 148 + 149 + ```typescript 150 + // Step 1: Add implements to ONE controller 151 + @Controller('pets') 152 + export class PetsController implements Pick<ControllerMethods, 'listPets'> { 153 + @Get() 154 + async listPets(@Query() query?: ListPetsData['query']) { ... } 155 + 156 + // Other methods remain untyped for now 157 + @Post() 158 + async createPet(@Body() body: any) { ... } 159 + } 160 + 161 + // Step 2: Gradually add more methods to the Pick union 162 + // TypeScript catches any signature mismatches immediately 163 + ``` 164 + 165 + ### Migration from Existing App 166 + 167 + ```typescript 168 + // BEFORE: existing controller, no generated types 169 + @Controller('pets') 170 + export class PetsController { 171 + @Get() 172 + async findAll(@Query('limit') limit?: number) { ... } 173 + } 174 + 175 + // AFTER: add implements, rename method to match operationId, use whole-object params 176 + @Controller('pets') 177 + export class PetsController implements Pick<ControllerMethods, 'listPets'> { 178 + @Get() 179 + async listPets(@Query() query?: ListPetsData['query']) { ... } 180 + } 181 + ``` 182 + 183 + --- 184 + 185 + ## Plugin Implementation 186 + 187 + ### File Structure 188 + 189 + ``` 190 + packages/openapi-ts/src/plugins/nestjs/ 191 + ├── plugin.ts # Core logic (~120-150 lines) 192 + ├── types.ts # Plugin type definitions (5 lines) 193 + ├── config.ts # Plugin registration (18 lines) 194 + └── index.ts # Re-exports (2 lines) 195 + ``` 196 + 197 + ### `types.ts` 198 + 199 + ```typescript 200 + import type { DefinePlugin, Plugin } from "@hey-api/shared"; 201 + 202 + export type UserConfig = Plugin.Name<"nestjs"> & 203 + Plugin.Hooks & 204 + Plugin.UserExports; 205 + 206 + export type NestJSPlugin = DefinePlugin<UserConfig, UserConfig>; 207 + ``` 208 + 209 + ### `config.ts` 210 + 211 + ```typescript 212 + import { definePluginConfig } from "@hey-api/shared"; 213 + 214 + import { handler } from "./plugin"; 215 + import type { NestJSPlugin } from "./types"; 216 + 217 + export const defaultConfig: NestJSPlugin["Config"] = { 218 + config: { 219 + includeInEntry: false, 220 + }, 221 + dependencies: ["@hey-api/typescript"], 222 + handler, 223 + name: "nestjs", 224 + }; 225 + 226 + export const defineConfig = definePluginConfig(defaultConfig); 227 + ``` 228 + 229 + ### `index.ts` 230 + 231 + ```typescript 232 + export { defaultConfig, defineConfig } from "./config"; 233 + export type { NestJSPlugin } from "./types"; 234 + ``` 235 + 236 + ### `plugin.ts` (Core Logic) 237 + 238 + ```typescript 239 + import type { IR } from "@hey-api/shared"; 240 + import { 241 + hasParameterGroupObjectRequired, 242 + operationResponsesMap, 243 + } from "@hey-api/shared"; 244 + 245 + import { $ } from "../../ts-dsl"; 246 + import type { NestJSPlugin } from "./types"; 247 + 248 + const operationToMethod = ({ 249 + operation, 250 + plugin, 251 + }: { 252 + operation: IR.OperationObject; 253 + plugin: NestJSPlugin["Instance"]; 254 + }) => { 255 + const funcType = $.type.func(); 256 + 257 + const symbolDataType = plugin.querySymbol({ 258 + category: "type", 259 + resource: "operation", 260 + resourceId: operation.id, 261 + role: "data", 262 + tool: "typescript", 263 + }); 264 + 265 + if (symbolDataType) { 266 + // Add path params 267 + if (operation.parameters?.path) { 268 + funcType.param((p) => 269 + p 270 + .name("path") 271 + .required(hasParameterGroupObjectRequired(operation.parameters!.path)) 272 + .type($.type(symbolDataType).idx($.type.literal("path"))), 273 + ); 274 + } 275 + 276 + // Add query params 277 + if (operation.parameters?.query) { 278 + funcType.param((p) => 279 + p 280 + .name("query") 281 + .required( 282 + hasParameterGroupObjectRequired(operation.parameters!.query), 283 + ) 284 + .type($.type(symbolDataType).idx($.type.literal("query"))), 285 + ); 286 + } 287 + 288 + // Add body 289 + if (operation.body) { 290 + funcType.param((p) => 291 + p 292 + .name("body") 293 + .required(operation.body!.required) 294 + .type($.type(symbolDataType).idx($.type.literal("body"))), 295 + ); 296 + } 297 + 298 + // Add headers 299 + if (operation.parameters?.header) { 300 + funcType.param((p) => 301 + p 302 + .name("headers") 303 + .required( 304 + hasParameterGroupObjectRequired(operation.parameters!.header), 305 + ) 306 + .type($.type(symbolDataType).idx($.type.literal("headers"))), 307 + ); 308 + } 309 + } 310 + 311 + // Build response type - use the response type alias (union of success bodies) 312 + const symbolResponseType = plugin.querySymbol({ 313 + category: "type", 314 + resource: "operation", 315 + resourceId: operation.id, 316 + role: "response", 317 + tool: "typescript", 318 + }); 319 + 320 + if (symbolResponseType) { 321 + funcType.returns( 322 + $.type("Promise", (t) => t.generic($.type(symbolResponseType))), 323 + ); 324 + } else { 325 + funcType.returns($.type("Promise", (t) => t.generic($.type("void")))); 326 + } 327 + 328 + return { 329 + name: operation.id, 330 + type: funcType, 331 + }; 332 + }; 333 + 334 + export const handler: NestJSPlugin["Handler"] = ({ plugin }) => { 335 + const symbolControllerMethods = plugin.symbol("ControllerMethods"); 336 + 337 + const type = $.type.object(); 338 + 339 + plugin.forEach( 340 + "operation", 341 + ({ operation }) => { 342 + const method = operationToMethod({ operation, plugin }); 343 + if (method) { 344 + type.prop(method.name, (p) => p.type(method.type)); 345 + } 346 + }, 347 + { 348 + order: "declarations", 349 + }, 350 + ); 351 + 352 + // Use interface (not type alias) to enable `implements` 353 + const node = $.type.alias(symbolControllerMethods).export().type(type); 354 + plugin.node(node); 355 + }; 356 + ``` 357 + 358 + > **Note:** The exact DSL calls for generating an `interface` vs `type` need verification during implementation. If `$.type.alias()` produces `type X = {...}` and we need `interface X {...}`, we may need to use a different DSL method or accept the `type` alias (which also works with `implements` in TypeScript). 359 + 360 + ### Registration (2 files to modify) 361 + 362 + **`packages/openapi-ts/src/plugins/config.ts`:** 363 + 364 + ```typescript 365 + import { defaultConfig as nestjs } from "./nestjs/config"; 366 + // ... add to defaultPluginConfigs map: 367 + // nestjs, 368 + ``` 369 + 370 + **`packages/openapi-ts/src/index.ts`:** 371 + 372 + ```typescript 373 + import type { NestJSPlugin } from "./plugins/nestjs/types"; 374 + // ... add to PluginConfigMap interface: 375 + // nestjs: NestJSPlugin['Types']; 376 + ``` 377 + 378 + --- 379 + 380 + ## Example Project 381 + 382 + ### Structure 383 + 384 + ``` 385 + examples/openapi-ts-nestjs/ 386 + ├── openapi-ts.config.ts # Config with 'nestjs' plugin 387 + ├── openapi.json # Petstore spec (same as Fastify example) 388 + ├── package.json 389 + ├── tsconfig.json 390 + ├── vite.config.ts # For Vitest 391 + ├── src/ 392 + │ ├── client/ # Generated output 393 + │ │ ├── nestjs.gen.ts # Generated controller interface 394 + │ │ ├── types.gen.ts # Generated types 395 + │ │ └── index.ts 396 + │ ├── pets/ 397 + │ │ └── pets.controller.ts # Implements ControllerMethods 398 + │ ├── app.module.ts # Root module 399 + │ └── main.ts # Bootstrap 400 + └── test/ 401 + └── pets.e2e-spec.ts # Supertest e2e test 402 + ``` 403 + 404 + ### Key Decisions 405 + 406 + - **Express adapter** (not Fastify) -- Express is the default NestJS adapter and avoids confusion with the existing Fastify plugin example 407 + - **Minimal dependencies** -- `@nestjs/core`, `@nestjs/common`, `@nestjs/platform-express`, `reflect-metadata`, `rxjs` 408 + - **Single controller** -- demonstrates the pattern without over-complicating 409 + - **E2E test** -- using `@nestjs/testing` + supertest to prove the typed contract works at runtime 410 + 411 + ### `openapi-ts.config.ts` 412 + 413 + ```typescript 414 + import { defineConfig } from "@hey-api/openapi-ts"; 415 + 416 + export default defineConfig({ 417 + input: "./openapi.json", 418 + output: { 419 + path: "./src/client", 420 + }, 421 + plugins: ["nestjs"], 422 + }); 423 + ``` 424 + 425 + ### `src/pets/pets.controller.ts` 426 + 427 + ```typescript 428 + import { Controller, Get, Post, Param, Query, Body } from "@nestjs/common"; 429 + import type { ControllerMethods } from "../client/nestjs.gen"; 430 + import type { 431 + CreatePetsData, 432 + ListPetsData, 433 + ShowPetByIdData, 434 + } from "../client/types.gen"; 435 + 436 + @Controller("pets") 437 + export class PetsController 438 + implements Pick<ControllerMethods, "listPets" | "createPets" | "showPetById"> 439 + { 440 + @Get() 441 + async listPets(@Query() query?: ListPetsData["query"]) { 442 + return []; 443 + } 444 + 445 + @Post() 446 + async createPets(@Body() body: CreatePetsData["body"]) { 447 + return; 448 + } 449 + 450 + @Get(":petId") 451 + async showPetById(@Param() path: ShowPetByIdData["path"]) { 452 + return { 453 + id: Number(path.petId), 454 + name: "Kitty", 455 + }; 456 + } 457 + } 458 + ``` 459 + 460 + --- 461 + 462 + ## Documentation 463 + 464 + Mirror Fastify docs at `docs/openapi-ts/plugins/nestjs.md` (~120 lines): 465 + 466 + 1. **Title + Beta Warning** 467 + 2. **About** -- "NestJS is a progressive Node.js framework..." 468 + 3. **Features** -- NestJS v10 support, type-safe controller interfaces, incremental adoption, zero runtime coupling 469 + 4. **Installation** -- config snippet with `'nestjs'` plugin 470 + 5. **Output: Controller Methods** -- generated interface + usage with `implements Pick<>` 471 + 6. **Migration** -- how to add `implements` to existing controllers 472 + 7. **Constraints** -- whole-object parameter style, `@Res()` incompatibility 473 + 8. **API** -- link to UserConfig type 474 + 475 + ### 30-Second Pitch 476 + 477 + > Add `'nestjs'` to your openapi-ts plugins. Generate. Add `implements Pick<ControllerMethods, ...>` to your controllers. TypeScript now enforces your API contract at compile time -- missing methods, wrong parameter types, and wrong return types all caught instantly. No decorators to learn, no restructuring required. 478 + 479 + --- 480 + 481 + ## Roadmap 482 + 483 + ### MVP (this PR) 484 + 485 + - [ ] Plugin files: `plugin.ts`, `types.ts`, `config.ts`, `index.ts` 486 + - [ ] Plugin registration in `config.ts` and `index.ts` 487 + - [ ] Flat `ControllerMethods` interface generation 488 + - [ ] Method signatures with typed path, query, body, headers params 489 + - [ ] `Promise<Response>` return types 490 + - [ ] Example project at `examples/openapi-ts-nestjs/` 491 + - [ ] Documentation at `docs/openapi-ts/plugins/nestjs.md` 492 + - [ ] Test snapshots 493 + 494 + ### v1.1 -- Tag-Based Grouping 495 + 496 + - [ ] `groupByTag?: boolean` config option (default: `true`) 497 + - [ ] Per-tag controller interfaces: `PetsController`, `StoreController` 498 + - [ ] `DefaultController` for untagged operations 499 + - [ ] First-tag-wins strategy for multi-tag operations 500 + - [ ] Update docs with grouping examples 501 + 502 + ### v2 -- Decorator & Validation Support 503 + 504 + - [ ] `@ApiResponse()` / `@ApiOperation()` decorator generation 505 + - [ ] DTO class generation with `class-validator` decorators 506 + - [ ] Service interface generation 507 + - [ ] Module scaffolding option 508 + 509 + --- 510 + 511 + ## Edge Cases to Handle 512 + 513 + | Edge Case | Handling | Source | 514 + | --------------------------- | ------------------------------------------------------------------- | ------------------------------------ | 515 + | No operationId | System auto-generates from method+path (e.g., `getPetsByPetId`) | Already handled by `operationToId()` | 516 + | Duplicate operation IDs | Numeric suffix appended (`result2`, `result3`) | Already handled by `operationToId()` | 517 + | Invalid TS identifier chars | Sanitized by `sanitizeNamespaceIdentifier()` | Already handled | 518 + | No parameters | Method has no params: `healthCheck(): Promise<HealthCheckResponse>` | Natural | 519 + | No response body (204) | Returns `Promise<void>` | Map void responses | 520 + | File uploads (multipart) | Body type is generated but may not match `@UploadedFile()` pattern | Document as limitation | 521 + | SSE/streaming | Return type won't match `Observable<MessageEvent>` | Document as limitation | 522 + | `@Res()` usage | Extra param breaks `implements` assignability | Document as limitation | 523 + 524 + --- 525 + 526 + ## Implementation Steps (Ordered) 527 + 528 + 1. **Create plugin files** -- `nestjs/plugin.ts`, `types.ts`, `config.ts`, `index.ts` 529 + 2. **Register plugin** -- add to `plugins/config.ts` and `PluginConfigMap` in `index.ts` 530 + 3. **Implement `plugin.ts`** -- iterate operations, build method signatures, generate interface 531 + 4. **Generate test snapshots** -- run against Petstore spec, verify output 532 + 5. **Create example project** -- `examples/openapi-ts-nestjs/` with working NestJS app 533 + 6. **Write documentation** -- `docs/openapi-ts/plugins/nestjs.md` 534 + 7. **Run full test suite** -- `pnpm test && pnpm typecheck && pnpm lint` 535 + 8. **Create changeset** -- for the new plugin 536 + 537 + --- 538 + 539 + ## Open Questions for Implementation 540 + 541 + 1. **`interface` vs `type` in DSL** -- Does `$.type.alias()` produce a `type` alias or can we emit an `interface`? Both work with `implements` in TypeScript, but `interface` is more idiomatic. Need to check DSL capabilities or use `type` if interface isn't supported. 542 + 543 + 2. **Response type symbol** -- The `role: 'response'` query may need verification. The Fastify plugin uses `role: 'responses'` (plural) which gives the status-code-indexed type. We want the flattened union. Need to check what `role: 'response'` (singular) returns vs `role: 'responses'` (plural). 544 + 545 + 3. **`$.type.func()` parameter syntax** -- The `TypeFuncTsDsl` has `ParamMixin` and `TypeReturnsMixin`. Need to verify the exact `.param()` and `.returns()` call syntax during implementation by examining how other plugins use function types.
+44 -30
docs/openapi-ts/plugins/nestjs-research.md
··· 7 7 ## Fastify Baseline 8 8 9 9 **Current implementation:** 10 + 10 11 - Location: `packages/openapi-ts/src/plugins/fastify/` 11 12 - Output: `RouteHandlers` interface mapping operation IDs to typed handlers 12 13 - Pattern: `RouteHandler<{ Body?, Headers?, Params?, Querystring?, Reply }>` ··· 18 19 19 20 ## NestJS vs Fastify 20 21 21 - | Aspect | Fastify | NestJS | 22 - |--------|---------|--------| 23 - | **Style** | Functional handlers | Class-based controllers | 24 - | **Typing** | Type generics | DTOs + decorators | 25 - | **DI** | Manual | Built-in container | 26 - | **Runtime lib** | `fastify-openapi-glue` | `@nestjs/swagger` | 27 - | **Module system** | None (flat object) | `@Module` required | 28 - | **Parameters** | Generic types | Decorator metadata (`@Param`, `@Query`, `@Body`) | 22 + | Aspect | Fastify | NestJS | 23 + | ----------------- | ---------------------- | ------------------------------------------------ | 24 + | **Style** | Functional handlers | Class-based controllers | 25 + | **Typing** | Type generics | DTOs + decorators | 26 + | **DI** | Manual | Built-in container | 27 + | **Runtime lib** | `fastify-openapi-glue` | `@nestjs/swagger` | 28 + | **Module system** | None (flat object) | `@Module` required | 29 + | **Parameters** | Generic types | Decorator metadata (`@Param`, `@Query`, `@Body`) | 29 30 30 31 ## Research Questions 31 32 32 33 ### High Priority (MVP) 33 34 34 35 **1. Output Format** 36 + 35 37 - Option A: Generate controller interface (recommended - mirrors Fastify pattern) 36 38 - Option B: Generate controller class with decorators 37 39 - Option C: Generate decorator metadata types ··· 39 41 **Recommendation**: Controller interface. Simple, type-safe, lets devs write decorators. 40 42 41 43 **2. Method Mapping** 44 + 42 45 - Map operation IDs to camelCase method names (matches SDK functions) 43 46 - Example: `GET /pets` with operationId `listPets` → `listPets()` method 44 47 45 48 **3. Type Structure** 46 49 Request parameters: 50 + 47 51 - Path params: `operation.parameters.path` → method param type 48 52 - Query params: `operation.parameters.query` → method param type 49 53 - Request body: `operation.body` → method param type 50 54 - Headers: `operation.parameters.header` → method param type 51 55 52 56 Response: 57 + 53 58 - Success responses: `operation.responses` → Promise return type 54 59 - Error responses: Include in union type 55 60 56 61 **4. Platform Support** 62 + 57 63 - NestJS supports both Express and Fastify adapters 58 64 - Generated types should be adapter-agnostic 59 65 - Let runtime decorators handle platform differences ··· 61 67 ### Medium Priority (Future) 62 68 63 69 **5. @nestjs/swagger Integration** 70 + 64 71 - Current: Generate standalone types 65 72 - Future: Optionally generate `@ApiResponse()`, `@ApiOperation()` decorators 66 73 - Requires config flag: `generateDecorators?: boolean` 67 74 68 75 **6. Validation** 76 + 69 77 - Current: Type-only validation via TypeScript 70 78 - Future: Generate `class-validator` decorators in DTOs 71 79 - Requires config flag: `generateValidation?: boolean` 72 80 73 81 **7. Module Generation** 82 + 74 83 - Current: Generate controller interface only 75 84 - Future: Generate full `@Module()` setup 76 85 - Requires config option: `moduleGeneration?: 'interface' | 'class' | 'module'` 77 86 78 87 **8. DI Layer** 88 + 79 89 - Current: Controller interface only 80 90 - Future: Generate service interfaces for business logic separation 81 91 - Common NestJS pattern: Controllers → Services → Repositories ··· 99 109 CreatePetsData, 100 110 CreatePetsResponses, 101 111 ShowPetByIdData, 102 - ShowPetByIdResponses 103 - } from './types.gen'; 112 + ShowPetByIdResponses, 113 + } from "./types.gen"; 104 114 105 115 export interface PetsController { 106 - listPets(query?: ListPetsData['query']): Promise<ListPetsResponses>; 107 - createPets(body: CreatePetsData['body']): Promise<CreatePetsResponses>; 108 - showPetById(params: ShowPetByIdData['path']): Promise<ShowPetByIdResponses>; 116 + listPets(query?: ListPetsData["query"]): Promise<ListPetsResponses>; 117 + createPets(body: CreatePetsData["body"]): Promise<CreatePetsResponses>; 118 + showPetById(params: ShowPetByIdData["path"]): Promise<ShowPetByIdResponses>; 109 119 } 110 120 ``` 111 121 ··· 113 123 114 124 ```typescript 115 125 // controllers/pets.controller.ts 116 - import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common'; 117 - import type { PetsController } from '../client/nestjs.gen'; 126 + import { Controller, Get, Post, Param, Query, Body } from "@nestjs/common"; 127 + import type { PetsController } from "../client/nestjs.gen"; 118 128 119 - @Controller('pets') 129 + @Controller("pets") 120 130 export class PetsControllerImpl implements PetsController { 121 131 @Get() 122 132 async listPets(@Query() query?) { ··· 128 138 return { id: 1 }; 129 139 } 130 140 131 - @Get(':petId') 141 + @Get(":petId") 132 142 async showPetById(@Param() params) { 133 - return { id: params.petId, name: 'Kitty' }; 143 + return { id: params.petId, name: "Kitty" }; 134 144 } 135 145 } 136 146 ``` ··· 160 170 161 171 Mirror Fastify docs style (~100 lines): 162 172 163 - ```markdown 173 + ````markdown 164 174 --- 165 175 title: NestJS Plugin 166 176 description: Generate NestJS controller interfaces from OpenAPI with type safety ··· 192 202 193 203 ```js 194 204 export default { 195 - input: 'openapi.json', 196 - output: 'src/client', 205 + input: "openapi.json", 206 + output: "src/client", 197 207 plugins: [ 198 - 'nestjs', // [!code ++] 208 + "nestjs", // [!code ++] 199 209 ], 200 210 }; 201 211 ``` 212 + ```` 202 213 203 214 ## Output 204 215 ··· 209 220 ::: code-group 210 221 211 222 ```ts [example] 212 - import { Controller, Get, Post } from '@nestjs/common'; 213 - import type { PetsController } from '../client/nestjs.gen'; 223 + import { Controller, Get, Post } from "@nestjs/common"; 224 + import type { PetsController } from "../client/nestjs.gen"; 214 225 215 - @Controller('pets') 226 + @Controller("pets") 216 227 export class PetsControllerImpl implements PetsController { 217 228 @Get() 218 229 async listPets(query?) { ··· 228 239 229 240 ```js [config] 230 241 export default { 231 - input: 'openapi.json', 232 - output: 'src/client', 242 + input: "openapi.json", 243 + output: "src/client", 233 244 plugins: [ 234 245 { 235 - name: 'nestjs', 246 + name: "nestjs", 236 247 }, 237 248 ], 238 249 }; ··· 243 254 ## API 244 255 245 256 See [UserConfig](https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/plugins/nestjs/types.ts). 246 - ``` 257 + 258 + ```` 247 259 248 260 ## Dependencies 249 261 ··· 287 299 ], 288 300 returns: 'Promise<OperationResponses>' 289 301 } 290 - ``` 302 + ```` 291 303 292 304 4. **Generate interface**: 305 + 293 306 ```typescript 294 307 export interface {ControllerName}Controller { 295 308 operation1(...params): Promise<Response1>; ··· 314 327 ## Critical Files Reference 315 328 316 329 **Study for implementation:** 330 + 317 331 - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/plugin.ts` - Core logic template 318 332 - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/types.ts` - UserConfig pattern 319 333 - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/config.ts` - Plugin registration