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.

Remove nestjs-research.md and implementation-plan-nestjs-plugin.md.

-870
-528
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 { ListPetsData, ShowPetByIdData, CreatePetsData } from '../client/types.gen'; 119 - 120 - @Controller('pets') 121 - export class PetsController implements Pick< 122 - ControllerMethods, 123 - 'listPets' | 'createPets' | 'showPetById' 124 - > { 125 - @Get() 126 - async listPets(@Query() query?: ListPetsData['query']) { 127 - return []; 128 - } 129 - 130 - @Post() 131 - async createPets(@Body() body: CreatePetsData['body']) { 132 - return { id: 1, name: body.name }; 133 - } 134 - 135 - @Get(':petId') 136 - async showPetById(@Param() path: ShowPetByIdData['path']) { 137 - return { id: path.petId, name: 'Kitty' }; 138 - } 139 - } 140 - ``` 141 - 142 - ### Incremental Adoption (DX Engineer's Key Insight) 143 - 144 - Unlike Fastify where you must provide ALL handlers at once, NestJS developers can adopt incrementally: 145 - 146 - ```typescript 147 - // Step 1: Add implements to ONE controller 148 - @Controller('pets') 149 - export class PetsController implements Pick<ControllerMethods, 'listPets'> { 150 - @Get() 151 - async listPets(@Query() query?: ListPetsData['query']) { ... } 152 - 153 - // Other methods remain untyped for now 154 - @Post() 155 - async createPet(@Body() body: any) { ... } 156 - } 157 - 158 - // Step 2: Gradually add more methods to the Pick union 159 - // TypeScript catches any signature mismatches immediately 160 - ``` 161 - 162 - ### Migration from Existing App 163 - 164 - ```typescript 165 - // BEFORE: existing controller, no generated types 166 - @Controller('pets') 167 - export class PetsController { 168 - @Get() 169 - async findAll(@Query('limit') limit?: number) { ... } 170 - } 171 - 172 - // AFTER: add implements, rename method to match operationId, use whole-object params 173 - @Controller('pets') 174 - export class PetsController implements Pick<ControllerMethods, 'listPets'> { 175 - @Get() 176 - async listPets(@Query() query?: ListPetsData['query']) { ... } 177 - } 178 - ``` 179 - 180 - --- 181 - 182 - ## Plugin Implementation 183 - 184 - ### File Structure 185 - 186 - ``` 187 - packages/openapi-ts/src/plugins/nestjs/ 188 - ├── plugin.ts # Core logic (~120-150 lines) 189 - ├── types.ts # Plugin type definitions (5 lines) 190 - ├── config.ts # Plugin registration (18 lines) 191 - └── index.ts # Re-exports (2 lines) 192 - ``` 193 - 194 - ### `types.ts` 195 - 196 - ```typescript 197 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 198 - 199 - export type UserConfig = Plugin.Name<'nestjs'> & Plugin.Hooks & Plugin.UserExports; 200 - 201 - export type NestJSPlugin = DefinePlugin<UserConfig, UserConfig>; 202 - ``` 203 - 204 - ### `config.ts` 205 - 206 - ```typescript 207 - import { definePluginConfig } from '@hey-api/shared'; 208 - 209 - import { handler } from './plugin'; 210 - import type { NestJSPlugin } from './types'; 211 - 212 - export const defaultConfig: NestJSPlugin['Config'] = { 213 - config: { 214 - includeInEntry: false, 215 - }, 216 - dependencies: ['@hey-api/typescript'], 217 - handler, 218 - name: 'nestjs', 219 - }; 220 - 221 - export const defineConfig = definePluginConfig(defaultConfig); 222 - ``` 223 - 224 - ### `index.ts` 225 - 226 - ```typescript 227 - export { defaultConfig, defineConfig } from './config'; 228 - export type { NestJSPlugin } from './types'; 229 - ``` 230 - 231 - ### `plugin.ts` (Core Logic) 232 - 233 - ```typescript 234 - import type { IR } from '@hey-api/shared'; 235 - import { hasParameterGroupObjectRequired, operationResponsesMap } from '@hey-api/shared'; 236 - 237 - import { $ } from '../../ts-dsl'; 238 - import type { NestJSPlugin } from './types'; 239 - 240 - const operationToMethod = ({ 241 - operation, 242 - plugin, 243 - }: { 244 - operation: IR.OperationObject; 245 - plugin: NestJSPlugin['Instance']; 246 - }) => { 247 - const funcType = $.type.func(); 248 - 249 - const symbolDataType = plugin.querySymbol({ 250 - category: 'type', 251 - resource: 'operation', 252 - resourceId: operation.id, 253 - role: 'data', 254 - tool: 'typescript', 255 - }); 256 - 257 - if (symbolDataType) { 258 - // Add path params 259 - if (operation.parameters?.path) { 260 - funcType.param((p) => 261 - p 262 - .name('path') 263 - .required(hasParameterGroupObjectRequired(operation.parameters!.path)) 264 - .type($.type(symbolDataType).idx($.type.literal('path'))), 265 - ); 266 - } 267 - 268 - // Add query params 269 - if (operation.parameters?.query) { 270 - funcType.param((p) => 271 - p 272 - .name('query') 273 - .required(hasParameterGroupObjectRequired(operation.parameters!.query)) 274 - .type($.type(symbolDataType).idx($.type.literal('query'))), 275 - ); 276 - } 277 - 278 - // Add body 279 - if (operation.body) { 280 - funcType.param((p) => 281 - p 282 - .name('body') 283 - .required(operation.body!.required) 284 - .type($.type(symbolDataType).idx($.type.literal('body'))), 285 - ); 286 - } 287 - 288 - // Add headers 289 - if (operation.parameters?.header) { 290 - funcType.param((p) => 291 - p 292 - .name('headers') 293 - .required(hasParameterGroupObjectRequired(operation.parameters!.header)) 294 - .type($.type(symbolDataType).idx($.type.literal('headers'))), 295 - ); 296 - } 297 - } 298 - 299 - // Build response type - use the response type alias (union of success bodies) 300 - const symbolResponseType = plugin.querySymbol({ 301 - category: 'type', 302 - resource: 'operation', 303 - resourceId: operation.id, 304 - role: 'response', 305 - tool: 'typescript', 306 - }); 307 - 308 - if (symbolResponseType) { 309 - funcType.returns($.type('Promise', (t) => t.generic($.type(symbolResponseType)))); 310 - } else { 311 - funcType.returns($.type('Promise', (t) => t.generic($.type('void')))); 312 - } 313 - 314 - return { 315 - name: operation.id, 316 - type: funcType, 317 - }; 318 - }; 319 - 320 - export const handler: NestJSPlugin['Handler'] = ({ plugin }) => { 321 - const symbolControllerMethods = plugin.symbol('ControllerMethods'); 322 - 323 - const type = $.type.object(); 324 - 325 - plugin.forEach( 326 - 'operation', 327 - ({ operation }) => { 328 - const method = operationToMethod({ operation, plugin }); 329 - if (method) { 330 - type.prop(method.name, (p) => p.type(method.type)); 331 - } 332 - }, 333 - { 334 - order: 'declarations', 335 - }, 336 - ); 337 - 338 - // Use interface (not type alias) to enable `implements` 339 - const node = $.type.alias(symbolControllerMethods).export().type(type); 340 - plugin.node(node); 341 - }; 342 - ``` 343 - 344 - > **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). 345 - 346 - ### Registration (2 files to modify) 347 - 348 - **`packages/openapi-ts/src/plugins/config.ts`:** 349 - 350 - ```typescript 351 - import { defaultConfig as nestjs } from './nestjs/config'; 352 - // ... add to defaultPluginConfigs map: 353 - // nestjs, 354 - ``` 355 - 356 - **`packages/openapi-ts/src/index.ts`:** 357 - 358 - ```typescript 359 - import type { NestJSPlugin } from './plugins/nestjs/types'; 360 - // ... add to PluginConfigMap interface: 361 - // nestjs: NestJSPlugin['Types']; 362 - ``` 363 - 364 - --- 365 - 366 - ## Example Project 367 - 368 - ### Structure 369 - 370 - ``` 371 - examples/openapi-ts-nestjs/ 372 - ├── openapi-ts.config.ts # Config with 'nestjs' plugin 373 - ├── openapi.json # Petstore spec (same as Fastify example) 374 - ├── package.json 375 - ├── tsconfig.json 376 - ├── vite.config.ts # For Vitest 377 - ├── src/ 378 - │ ├── client/ # Generated output 379 - │ │ ├── nestjs.gen.ts # Generated controller interface 380 - │ │ ├── types.gen.ts # Generated types 381 - │ │ └── index.ts 382 - │ ├── pets/ 383 - │ │ └── pets.controller.ts # Implements ControllerMethods 384 - │ ├── app.module.ts # Root module 385 - │ └── main.ts # Bootstrap 386 - └── test/ 387 - └── pets.e2e-spec.ts # Supertest e2e test 388 - ``` 389 - 390 - ### Key Decisions 391 - 392 - - **Express adapter** (not Fastify) -- Express is the default NestJS adapter and avoids confusion with the existing Fastify plugin example 393 - - **Minimal dependencies** -- `@nestjs/core`, `@nestjs/common`, `@nestjs/platform-express`, `reflect-metadata`, `rxjs` 394 - - **Single controller** -- demonstrates the pattern without over-complicating 395 - - **E2E test** -- using `@nestjs/testing` + supertest to prove the typed contract works at runtime 396 - 397 - ### `openapi-ts.config.ts` 398 - 399 - ```typescript 400 - import { defineConfig } from '@hey-api/openapi-ts'; 401 - 402 - export default defineConfig({ 403 - input: './openapi.json', 404 - output: { 405 - path: './src/client', 406 - }, 407 - plugins: ['nestjs'], 408 - }); 409 - ``` 410 - 411 - ### `src/pets/pets.controller.ts` 412 - 413 - ```typescript 414 - import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common'; 415 - import type { ControllerMethods } from '../client/nestjs.gen'; 416 - import type { CreatePetsData, ListPetsData, ShowPetByIdData } from '../client/types.gen'; 417 - 418 - @Controller('pets') 419 - export class PetsController implements Pick< 420 - ControllerMethods, 421 - 'listPets' | 'createPets' | 'showPetById' 422 - > { 423 - @Get() 424 - async listPets(@Query() query?: ListPetsData['query']) { 425 - return []; 426 - } 427 - 428 - @Post() 429 - async createPets(@Body() body: CreatePetsData['body']) { 430 - return; 431 - } 432 - 433 - @Get(':petId') 434 - async showPetById(@Param() path: ShowPetByIdData['path']) { 435 - return { 436 - id: Number(path.petId), 437 - name: 'Kitty', 438 - }; 439 - } 440 - } 441 - ``` 442 - 443 - --- 444 - 445 - ## Documentation 446 - 447 - Mirror Fastify docs at `docs/openapi-ts/plugins/nestjs.md` (~120 lines): 448 - 449 - 1. **Title + Beta Warning** 450 - 2. **About** -- "NestJS is a progressive Node.js framework..." 451 - 3. **Features** -- NestJS v10 support, type-safe controller interfaces, incremental adoption, zero runtime coupling 452 - 4. **Installation** -- config snippet with `'nestjs'` plugin 453 - 5. **Output: Controller Methods** -- generated interface + usage with `implements Pick<>` 454 - 6. **Migration** -- how to add `implements` to existing controllers 455 - 7. **Constraints** -- whole-object parameter style, `@Res()` incompatibility 456 - 8. **API** -- link to UserConfig type 457 - 458 - ### 30-Second Pitch 459 - 460 - > 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. 461 - 462 - --- 463 - 464 - ## Roadmap 465 - 466 - ### MVP (this PR) 467 - 468 - - [ ] Plugin files: `plugin.ts`, `types.ts`, `config.ts`, `index.ts` 469 - - [ ] Plugin registration in `config.ts` and `index.ts` 470 - - [ ] Flat `ControllerMethods` interface generation 471 - - [ ] Method signatures with typed path, query, body, headers params 472 - - [ ] `Promise<Response>` return types 473 - - [ ] Example project at `examples/openapi-ts-nestjs/` 474 - - [ ] Documentation at `docs/openapi-ts/plugins/nestjs.md` 475 - - [ ] Test snapshots 476 - 477 - ### v1.1 -- Tag-Based Grouping 478 - 479 - - [ ] `groupByTag?: boolean` config option (default: `true`) 480 - - [ ] Per-tag controller interfaces: `PetsController`, `StoreController` 481 - - [ ] `DefaultController` for untagged operations 482 - - [ ] First-tag-wins strategy for multi-tag operations 483 - - [ ] Update docs with grouping examples 484 - 485 - ### v2 -- Decorator & Validation Support 486 - 487 - - [ ] `@ApiResponse()` / `@ApiOperation()` decorator generation 488 - - [ ] DTO class generation with `class-validator` decorators 489 - - [ ] Service interface generation 490 - - [ ] Module scaffolding option 491 - 492 - --- 493 - 494 - ## Edge Cases to Handle 495 - 496 - | Edge Case | Handling | Source | 497 - | --------------------------- | ------------------------------------------------------------------- | ------------------------------------ | 498 - | No operationId | System auto-generates from method+path (e.g., `getPetsByPetId`) | Already handled by `operationToId()` | 499 - | Duplicate operation IDs | Numeric suffix appended (`result2`, `result3`) | Already handled by `operationToId()` | 500 - | Invalid TS identifier chars | Sanitized by `sanitizeNamespaceIdentifier()` | Already handled | 501 - | No parameters | Method has no params: `healthCheck(): Promise<HealthCheckResponse>` | Natural | 502 - | No response body (204) | Returns `Promise<void>` | Map void responses | 503 - | File uploads (multipart) | Body type is generated but may not match `@UploadedFile()` pattern | Document as limitation | 504 - | SSE/streaming | Return type won't match `Observable<MessageEvent>` | Document as limitation | 505 - | `@Res()` usage | Extra param breaks `implements` assignability | Document as limitation | 506 - 507 - --- 508 - 509 - ## Implementation Steps (Ordered) 510 - 511 - 1. **Create plugin files** -- `nestjs/plugin.ts`, `types.ts`, `config.ts`, `index.ts` 512 - 2. **Register plugin** -- add to `plugins/config.ts` and `PluginConfigMap` in `index.ts` 513 - 3. **Implement `plugin.ts`** -- iterate operations, build method signatures, generate interface 514 - 4. **Generate test snapshots** -- run against Petstore spec, verify output 515 - 5. **Create example project** -- `examples/openapi-ts-nestjs/` with working NestJS app 516 - 6. **Write documentation** -- `docs/openapi-ts/plugins/nestjs.md` 517 - 7. **Run full test suite** -- `pnpm test && pnpm typecheck && pnpm lint` 518 - 8. **Create changeset** -- for the new plugin 519 - 520 - --- 521 - 522 - ## Open Questions for Implementation 523 - 524 - 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. 525 - 526 - 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). 527 - 528 - 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.
-342
docs/openapi-ts/plugins/nestjs-research.md
··· 1 - # NestJS Plugin Research & Requirements 2 - 3 - ## Context 4 - 5 - Investigate NestJS plugin following Fastify plugin pattern. Generate type-safe controller interfaces from OpenAPI specs for NestJS applications. 6 - 7 - ## Fastify Baseline 8 - 9 - **Current implementation:** 10 - 11 - - Location: `packages/openapi-ts/src/plugins/fastify/` 12 - - Output: `RouteHandlers` interface mapping operation IDs to typed handlers 13 - - Pattern: `RouteHandler<{ Body?, Headers?, Params?, Querystring?, Reply }>` 14 - - Size: ~155 lines core logic 15 - - Deps: `@hey-api/typescript` plugin 16 - - Runtime: Uses `fastify-openapi-glue` library for route registration 17 - - Example: `examples/openapi-ts-fastify/` 18 - - Docs: `docs/openapi-ts/plugins/fastify.md` (~100 lines) 19 - 20 - ## NestJS vs Fastify 21 - 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`) | 30 - 31 - ## Research Questions 32 - 33 - ### High Priority (MVP) 34 - 35 - **1. Output Format** 36 - 37 - - Option A: Generate controller interface (recommended - mirrors Fastify pattern) 38 - - Option B: Generate controller class with decorators 39 - - Option C: Generate decorator metadata types 40 - 41 - **Recommendation**: Controller interface. Simple, type-safe, lets devs write decorators. 42 - 43 - **2. Method Mapping** 44 - 45 - - Map operation IDs to camelCase method names (matches SDK functions) 46 - - Example: `GET /pets` with operationId `listPets` → `listPets()` method 47 - 48 - **3. Type Structure** 49 - Request parameters: 50 - 51 - - Path params: `operation.parameters.path` → method param type 52 - - Query params: `operation.parameters.query` → method param type 53 - - Request body: `operation.body` → method param type 54 - - Headers: `operation.parameters.header` → method param type 55 - 56 - Response: 57 - 58 - - Success responses: `operation.responses` → Promise return type 59 - - Error responses: Include in union type 60 - 61 - **4. Platform Support** 62 - 63 - - NestJS supports both Express and Fastify adapters 64 - - Generated types should be adapter-agnostic 65 - - Let runtime decorators handle platform differences 66 - 67 - ### Medium Priority (Future) 68 - 69 - **5. @nestjs/swagger Integration** 70 - 71 - - Current: Generate standalone types 72 - - Future: Optionally generate `@ApiResponse()`, `@ApiOperation()` decorators 73 - - Requires config flag: `generateDecorators?: boolean` 74 - 75 - **6. Validation** 76 - 77 - - Current: Type-only validation via TypeScript 78 - - Future: Generate `class-validator` decorators in DTOs 79 - - Requires config flag: `generateValidation?: boolean` 80 - 81 - **7. Module Generation** 82 - 83 - - Current: Generate controller interface only 84 - - Future: Generate full `@Module()` setup 85 - - Requires config option: `moduleGeneration?: 'interface' | 'class' | 'module'` 86 - 87 - **8. DI Layer** 88 - 89 - - Current: Controller interface only 90 - - Future: Generate service interfaces for business logic separation 91 - - Common NestJS pattern: Controllers → Services → Repositories 92 - 93 - ### Low Priority (v2+) 94 - 95 - 9. Guards/interceptors/pipes type generation 96 - 10. Exception filters for typed error responses 97 - 11. SwaggerModule auto-configuration 98 - 12. Validation pipe integration 99 - 100 - ## Proposed MVP Output 101 - 102 - ### Generated Interface 103 - 104 - ```typescript 105 - // client/nestjs.gen.ts 106 - import type { 107 - ListPetsData, 108 - ListPetsResponses, 109 - CreatePetsData, 110 - CreatePetsResponses, 111 - ShowPetByIdData, 112 - ShowPetByIdResponses, 113 - } from './types.gen'; 114 - 115 - export interface PetsController { 116 - listPets(query?: ListPetsData['query']): Promise<ListPetsResponses>; 117 - createPets(body: CreatePetsData['body']): Promise<CreatePetsResponses>; 118 - showPetById(params: ShowPetByIdData['path']): Promise<ShowPetByIdResponses>; 119 - } 120 - ``` 121 - 122 - ### Usage Pattern 123 - 124 - ```typescript 125 - // controllers/pets.controller.ts 126 - import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common'; 127 - import type { PetsController } from '../client/nestjs.gen'; 128 - 129 - @Controller('pets') 130 - export class PetsControllerImpl implements PetsController { 131 - @Get() 132 - async listPets(@Query() query?) { 133 - return []; 134 - } 135 - 136 - @Post() 137 - async createPets(@Body() body) { 138 - return { id: 1 }; 139 - } 140 - 141 - @Get(':petId') 142 - async showPetById(@Param() params) { 143 - return { id: params.petId, name: 'Kitty' }; 144 - } 145 - } 146 - ``` 147 - 148 - ## Example Structure 149 - 150 - ``` 151 - examples/openapi-ts-nestjs/ 152 - ├── openapi-ts.config.ts # plugins: ['nestjs', '@hey-api/sdk'] 153 - ├── openapi.json # Petstore spec (same as Fastify example) 154 - ├── package.json # @nestjs/core, @nestjs/common, @nestjs/platform-express 155 - ├── src/ 156 - │ ├── client/ 157 - │ │ ├── nestjs.gen.ts # Generated controller interface 158 - │ │ ├── types.gen.ts # Shared types (from TypeScript plugin) 159 - │ │ ├── sdk.gen.ts # Client SDK (from SDK plugin) 160 - │ │ └── index.ts 161 - │ ├── controllers/ 162 - │ │ └── pets.controller.ts # Implements PetsController interface 163 - │ ├── app.module.ts # NestJS module setup 164 - │ └── main.ts # Bootstrap app 165 - └── test/ 166 - └── pets.test.ts # Supertest integration tests 167 - ``` 168 - 169 - ## Documentation Structure 170 - 171 - Mirror Fastify docs style (~100 lines): 172 - 173 - ````markdown 174 - --- 175 - title: NestJS Plugin 176 - description: Generate NestJS controller interfaces from OpenAPI with type safety 177 - --- 178 - 179 - # NestJS 180 - 181 - ::: warning 182 - NestJS plugin is currently in beta. Interface might change before stable. 183 - ::: 184 - 185 - ## About 186 - 187 - [NestJS](https://nestjs.com) is a progressive Node.js framework for building 188 - efficient, reliable server-side applications. 189 - 190 - The NestJS plugin generates type-safe controller interfaces from OpenAPI specs. 191 - 192 - ## Features 193 - 194 - - NestJS v10 support 195 - - seamless integration with @hey-api/openapi-ts ecosystem 196 - - type-safe controller interfaces 197 - - minimal learning curve 198 - 199 - ## Installation 200 - 201 - Add `nestjs` to your plugins: 202 - 203 - ```js 204 - export default { 205 - input: 'openapi.json', 206 - output: 'src/client', 207 - plugins: [ 208 - 'nestjs', // [!code ++] 209 - ], 210 - }; 211 - ``` 212 - ```` 213 - 214 - ## Output 215 - 216 - ### Controller Types 217 - 218 - Generated controller interfaces from all endpoints. Follows SDK naming conventions. 219 - 220 - ::: code-group 221 - 222 - ```ts [example] 223 - import { Controller, Get, Post } from '@nestjs/common'; 224 - import type { PetsController } from '../client/nestjs.gen'; 225 - 226 - @Controller('pets') 227 - export class PetsControllerImpl implements PetsController { 228 - @Get() 229 - async listPets(query?) { 230 - return []; 231 - } 232 - 233 - @Post() 234 - async createPets(body) { 235 - return { id: 1 }; 236 - } 237 - } 238 - ``` 239 - 240 - ```js [config] 241 - export default { 242 - input: 'openapi.json', 243 - output: 'src/client', 244 - plugins: [ 245 - { 246 - name: 'nestjs', 247 - }, 248 - ], 249 - }; 250 - ``` 251 - 252 - ::: 253 - 254 - ## API 255 - 256 - See [UserConfig](https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/plugins/nestjs/types.ts). 257 - 258 - ```` 259 - 260 - ## Dependencies 261 - 262 - ### Plugin (dev) 263 - - `@hey-api/typescript` - Base type generation (required dependency) 264 - 265 - ### Example (runtime) 266 - - `@nestjs/core` (^10.0.0) 267 - - `@nestjs/common` (^10.0.0) 268 - - `@nestjs/platform-express` (^10.0.0) or `@nestjs/platform-fastify` 269 - 270 - ### Optional (future) 271 - - `@nestjs/swagger` - For decorator generation 272 - - `class-validator` - For DTO validation decorators 273 - - `class-transformer` - For DTO transformation 274 - 275 - ## Implementation Approach 276 - 277 - 1. **Copy Fastify plugin structure** as baseline: 278 - - `packages/openapi-ts/src/plugins/nestjs/plugin.ts` 279 - - `packages/openapi-ts/src/plugins/nestjs/types.ts` 280 - - `packages/openapi-ts/src/plugins/nestjs/config.ts` 281 - - `packages/openapi-ts/src/plugins/nestjs/index.ts` 282 - 283 - 2. **Adapt type generation** for controller interface pattern: 284 - - Iterate operations via `plugin.forEach('operation')` 285 - - Extract request params (path, query, body, headers) 286 - - Extract response types 287 - - Generate method signature with typed params and Promise return 288 - 289 - 3. **Parameter mapping**: 290 - ```typescript 291 - // For each operation: 292 - { 293 - methodName: operationId, 294 - params: [ 295 - ...(hasPathParams ? ['params: OperationData["path"]'] : []), 296 - ...(hasQueryParams ? ['query?: OperationData["query"]'] : []), 297 - ...(hasBody ? ['body: OperationData["body"]'] : []), 298 - ...(hasHeaders ? ['headers?: OperationData["headers"]'] : []), 299 - ], 300 - returns: 'Promise<OperationResponses>' 301 - } 302 - ```` 303 - 304 - 4. **Generate interface**: 305 - 306 - ```typescript 307 - export interface {ControllerName}Controller { 308 - operation1(...params): Promise<Response1>; 309 - operation2(...params): Promise<Response2>; 310 - } 311 - ``` 312 - 313 - 5. **Create example** at `examples/openapi-ts-nestjs/` 314 - 315 - 6. **Write docs** at `docs/openapi-ts/plugins/nestjs.md` 316 - 317 - ## Deferred Features (v2+) 318 - 319 - - Full `@ApiResponse()`, `@ApiOperation()` decorator generation 320 - - `class-validator` decorators in generated DTOs 321 - - Full module generation with `@Module()` decorator 322 - - Service interface generation for DI layer 323 - - Guard/interceptor/pipe type generation 324 - - Exception filter types 325 - - SwaggerModule auto-configuration 326 - 327 - ## Critical Files Reference 328 - 329 - **Study for implementation:** 330 - 331 - - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/plugin.ts` - Core logic template 332 - - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/types.ts` - UserConfig pattern 333 - - `/Users/undgrnd/Code/openapi-ts/packages/openapi-ts/src/plugins/fastify/config.ts` - Plugin registration 334 - - `/Users/undgrnd/Code/openapi-ts/examples/openapi-ts-fastify/src/handlers.ts` - Handler pattern 335 - - `/Users/undgrnd/Code/openapi-ts/docs/openapi-ts/plugins/fastify.md` - Docs template 336 - 337 - ## Next Steps 338 - 339 - 1. Review this research doc 340 - 2. Clarify any open questions with team 341 - 3. Get approval for MVP scope (controller interface only) 342 - 4. Proceed with implementation following Fastify plugin structure