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.

feat: add README documentation for NestJS plugin design and constraints.

+51 -660
-400
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 - > **Status: Implemented.** This document reflects the final decisions and actual implementation. 6 - 7 - ## Executive Summary 8 - 9 - Generate type-safe controller method types from OpenAPI specs for NestJS applications. The plugin generates **per-tag `ControllerMethods` type aliases** mapping operation IDs to typed method signatures. NestJS developers add `implements Pick<PetsControllerMethods, ...>` to their controller classes for compile-time contract enforcement. 10 - 11 - **Why per-tag grouping (not flat)?** Tag-based grouping was originally deferred to v1.1, but was implemented directly because it maps naturally to NestJS's one-controller-per-resource pattern. Operations are grouped by their first OpenAPI tag into `{PascalTag}ControllerMethods` types. Untagged operations go to `DefaultControllerMethods`. 12 - 13 - --- 14 - 15 - ## Debate Resolution Summary 16 - 17 - | Decision | Resolution | Winning Argument | 18 - | ---------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 19 - | Output format | Per-tag `{Tag}ControllerMethods` type aliases | Tag grouping maps 1:1 to NestJS controllers; implemented directly instead of deferring | 20 - | `type` vs `interface` | `type` alias | DSL naturally produces `type` aliases; works with `implements` in TypeScript just like `interface` | 21 - | Parameters | Individual params: `path`, `query`, `body`, `headers` | All agreed: maps 1:1 to NestJS `@Param()`, `@Query()`, `@Body()`, `@Headers()` | 22 - | Parameter ordering | Required before optional | Prevents TS errors from optional params preceding required ones | 23 - | Return type | `Promise<OperationResponse>` (union of success responses) | DX Engineer: controllers return values, not status-code maps | 24 - | Tag grouping | Implemented in MVP | Natural fit for NestJS controller-per-resource pattern | 25 - | Decorator generation | Deferred to v2 | All agreed: not MVP scope | 26 - | Config options | None for MVP | Arch Lead: zero config = fastest path to value | 27 - | `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 | 28 - 29 - --- 30 - 31 - ## Critical Design Decision: The `implements` Constraint 32 - 33 - The Devil's Advocate identified the most important technical finding: 34 - 35 - **TypeScript `implements` works with NestJS decorated parameters**, but only if the user adopts whole-object parameter style: 36 - 37 - ```typescript 38 - // WORKS with implements - whole-object style (recommended) 39 - @Get(':petId') 40 - async showPetById(@Param() path: ShowPetByIdData['path']): Promise<ShowPetByIdResponse> { ... } 41 - 42 - // DOES NOT WORK with implements - decomposed style 43 - @Get(':petId') 44 - async showPetById(@Param('petId') petId: string): Promise<ShowPetByIdResponse> { ... } 45 - ``` 46 - 47 - The whole-object style is a recognized NestJS pattern and arguably cleaner. This constraint is prominently documented. 48 - 49 - **Known limitations (documented):** 50 - 51 - - Methods using `@Res()` for raw response access are incompatible (extra parameter breaks assignability) 52 - - File upload operations using `@UploadedFile()` may need to bypass `implements` for specific methods 53 - - SSE endpoints returning `Observable<MessageEvent>` don't match `Promise<T>` return type 54 - 55 - --- 56 - 57 - ## Actual Output Format 58 - 59 - ### Generated `nestjs.gen.ts` 60 - 61 - For the Petstore spec (with CRUD operations): 62 - 63 - ```typescript 64 - // This file is auto-generated by @hey-api/openapi-ts 65 - 66 - import type { 67 - CreatePetData, 68 - CreatePetResponse, 69 - DeletePetData, 70 - DeletePetResponse, 71 - GetInventoryResponse, 72 - ListPetsData, 73 - ListPetsResponse, 74 - ShowPetByIdData, 75 - ShowPetByIdResponse, 76 - UpdatePetData, 77 - UpdatePetResponse, 78 - } from './types.gen'; 79 - 80 - export type PetsControllerMethods = { 81 - createPet: (body: CreatePetData['body']) => Promise<CreatePetResponse>; 82 - deletePet: (path: DeletePetData['path']) => Promise<DeletePetResponse>; 83 - listPets: (query?: ListPetsData['query']) => Promise<ListPetsResponse>; 84 - showPetById: (path: ShowPetByIdData['path']) => Promise<ShowPetByIdResponse>; 85 - updatePet: ( 86 - path: UpdatePetData['path'], 87 - body: UpdatePetData['body'], 88 - ) => Promise<UpdatePetResponse>; 89 - }; 90 - 91 - export type StoreControllerMethods = { 92 - getInventory: () => Promise<GetInventoryResponse>; 93 - }; 94 - ``` 95 - 96 - ### Design Decisions in Output 97 - 98 - 1. **`type` alias (not `interface`)** -- DSL naturally produces `type` aliases; both work with `implements` in TypeScript 99 - 2. **Per-tag grouping** -- operations grouped by first OpenAPI tag into `{PascalTag}ControllerMethods` 100 - 3. **Individual method parameters** -- `path`, `query`, `body`, `headers` matching NestJS decorator extraction 101 - 4. **Required-before-optional sorting** -- params sorted so required come first, preventing TS signature errors 102 - 5. **Optional params use `?`** -- uses `hasParameterGroupObjectRequired()` to determine optionality 103 - 6. **Return type is `Promise<OperationResponse>`** -- union of success response body types, not status-code-indexed map 104 - 7. **No external imports** -- unlike Fastify which imports `RouteHandler` from the `fastify` package, NestJS output is pure TypeScript. Zero runtime dependencies 105 - 8. **Untagged operations** -- grouped under `DefaultControllerMethods` 106 - 107 - ### Parameter Mapping 108 - 109 - ``` 110 - OpenAPI → Method Parameter 111 - ───────────────────────────────────────────── 112 - operation.parameters.path → path: OperationData['path'] 113 - operation.parameters.query → query?: OperationData['query'] 114 - operation.body → body: OperationData['body'] 115 - operation.parameters.header→ headers?: OperationData['headers'] 116 - ``` 117 - 118 - Parameters are collected then sorted: required before optional. This prevents TypeScript errors where an optional parameter would precede a required one (e.g., `(query?: T, body: U)` is invalid). 119 - 120 - ### Response Type Mapping 121 - 122 - Uses `operationResponsesMap()` to check for responses, then queries the `role: 'response'` symbol (the union of success response bodies), NOT the `role: 'responses'` status-code-indexed type. Wraps in `Promise<T>`, falls back to `Promise<void>` when no response symbol exists. 123 - 124 - --- 125 - 126 - ## Usage Patterns 127 - 128 - ### Basic Usage 129 - 130 - ```typescript 131 - import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common'; 132 - import type { PetsControllerMethods } from '../client/nestjs.gen'; 133 - import type { ListPetsData, ShowPetByIdData, CreatePetData } from '../client/types.gen'; 134 - 135 - @Controller('pets') 136 - export class PetsController implements Pick< 137 - PetsControllerMethods, 138 - 'listPets' | 'createPet' | 'showPetById' 139 - > { 140 - @Get() 141 - async listPets(@Query() query?: ListPetsData['query']) { 142 - return []; 143 - } 144 - 145 - @Post() 146 - async createPet(@Body() body: CreatePetData['body']) { 147 - return { id: 1, name: body.name }; 148 - } 149 - 150 - @Get(':petId') 151 - async showPetById(@Param() path: ShowPetByIdData['path']) { 152 - return { id: Number(path.petId), name: 'Kitty' }; 153 - } 154 - } 155 - ``` 156 - 157 - ### Incremental Adoption (DX Engineer's Key Insight) 158 - 159 - Unlike Fastify where you must provide ALL handlers at once, NestJS developers can adopt incrementally: 160 - 161 - ```typescript 162 - // Step 1: Add implements to ONE controller 163 - @Controller('pets') 164 - export class PetsController implements Pick<PetsControllerMethods, 'listPets'> { 165 - @Get() 166 - async listPets(@Query() query?: ListPetsData['query']) { ... } 167 - 168 - // Other methods remain untyped for now 169 - @Post() 170 - async createPet(@Body() body: any) { ... } 171 - } 172 - 173 - // Step 2: Gradually add more methods to the Pick union 174 - // TypeScript catches any signature mismatches immediately 175 - ``` 176 - 177 - ### Migration from Existing App 178 - 179 - ```typescript 180 - // BEFORE: existing controller, no generated types 181 - @Controller('pets') 182 - export class PetsController { 183 - @Get() 184 - async findAll(@Query('limit') limit?: number) { ... } 185 - } 186 - 187 - // AFTER: add implements, rename method to match operationId, use whole-object params 188 - @Controller('pets') 189 - export class PetsController implements Pick<PetsControllerMethods, 'listPets'> { 190 - @Get() 191 - async listPets(@Query() query?: ListPetsData['query']) { ... } 192 - } 193 - ``` 194 - 195 - --- 196 - 197 - ## Plugin Implementation 198 - 199 - ### File Structure 200 - 201 - ``` 202 - packages/openapi-ts/src/plugins/nestjs/ 203 - ├── plugin.ts # Core logic (143 lines) 204 - ├── types.ts # Plugin type definitions (5 lines) 205 - ├── config.ts # Plugin registration (18 lines) 206 - └── index.ts # Re-exports (2 lines) 207 - ``` 208 - 209 - ### `types.ts` 210 - 211 - ```typescript 212 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 213 - 214 - export type UserConfig = Plugin.Name<'nestjs'> & Plugin.Hooks & Plugin.UserExports; 215 - 216 - export type NestJSPlugin = DefinePlugin<UserConfig, UserConfig>; 217 - ``` 218 - 219 - ### `config.ts` 220 - 221 - ```typescript 222 - import { definePluginConfig } from '@hey-api/shared'; 223 - 224 - import { handler } from './plugin'; 225 - import type { NestJSPlugin } from './types'; 226 - 227 - export const defaultConfig: NestJSPlugin['Config'] = { 228 - config: { 229 - includeInEntry: false, 230 - }, 231 - dependencies: ['@hey-api/typescript'], 232 - handler, 233 - name: 'nestjs', 234 - }; 235 - 236 - export const defineConfig = definePluginConfig(defaultConfig); 237 - ``` 238 - 239 - ### `index.ts` 240 - 241 - ```typescript 242 - export { defaultConfig, defineConfig } from './config'; 243 - export type { NestJSPlugin } from './types'; 244 - ``` 245 - 246 - ### `plugin.ts` (Core Logic) 247 - 248 - The handler groups operations by first tag, builds method signatures per operation, and emits one type alias per tag: 249 - 250 - ```typescript 251 - export const handler: NestJSPlugin['Handler'] = ({ plugin }) => { 252 - const operationsByTag = new Map<string, Array<{ name: string; type: ... }>>(); 253 - 254 - plugin.forEach('operation', ({ operation, tags }) => { 255 - const tag = tags?.[0] ?? 'default'; 256 - // ... collect methods per tag 257 - }, { order: 'declarations' }); 258 - 259 - for (const [tag, methods] of operationsByTag) { 260 - const pascalTag = toCase(tag, 'PascalCase'); 261 - // emit: export type {PascalTag}ControllerMethods = { ... } 262 - } 263 - }; 264 - ``` 265 - 266 - Key implementation details: 267 - 268 - - `operationToMethod()` builds each method's function type 269 - - Parameters collected into array, then sorted (required before optional) 270 - - Uses `plugin.querySymbol({ role: 'data' })` for param types 271 - - Uses `plugin.querySymbol({ role: 'response' })` for return types 272 - - Uses `operationResponsesMap()` to verify responses exist before referencing response symbol 273 - 274 - ### Registration 275 - 276 - **`packages/openapi-ts/src/plugins/config.ts`:** 277 - 278 - ```typescript 279 - import { defaultConfig as nestjs } from './nestjs/config'; 280 - // added to defaultPluginConfigs map 281 - ``` 282 - 283 - **`packages/openapi-ts/src/index.ts`:** 284 - 285 - ```typescript 286 - import type { NestJSPlugin } from './plugins/nestjs/types'; 287 - // added to PluginConfigMap: nestjs: NestJSPlugin['Types'] 288 - ``` 289 - 290 - --- 291 - 292 - ## Example Project 293 - 294 - ### Structure 295 - 296 - ``` 297 - examples/openapi-ts-nestjs/ 298 - ├── openapi-ts.config.ts # plugins: ['nestjs', '@hey-api/sdk', ...] 299 - ├── openapi.json # Extended Petstore spec (CRUD + store) 300 - ├── eslint.config.js # NestJS-specific linting 301 - ├── package.json 302 - ├── tsconfig.json 303 - ├── vite.config.ts # For Vitest 304 - ├── src/ 305 - │ ├── client/ # Generated output 306 - │ │ ├── nestjs.gen.ts # Generated controller method types 307 - │ │ ├── types.gen.ts # Generated types 308 - │ │ ├── sdk.gen.ts # Client SDK 309 - │ │ └── index.ts 310 - │ ├── pets/ 311 - │ │ ├── pets.controller.ts # Implements PetsControllerMethods 312 - │ │ ├── dto/ # class-validator DTOs 313 - │ │ └── pets.module.ts 314 - │ ├── store/ 315 - │ │ ├── store.controller.ts # Implements StoreControllerMethods 316 - │ │ └── store.module.ts 317 - │ ├── common/ 318 - │ │ └── filters/ 319 - │ │ └── http-exception.filter.ts 320 - │ ├── app.module.ts # Root module 321 - │ └── main.ts # Bootstrap with ValidationPipe 322 - └── test/ 323 - └── pets.test.ts # Integration tests 324 - ``` 325 - 326 - ### Key Decisions 327 - 328 - - **Express adapter** (not Fastify) -- Express is the default NestJS adapter and avoids confusion with the existing Fastify plugin example 329 - - **@nestjs/swagger** -- OpenAPI documentation integration 330 - - **class-validator DTOs** -- request validation with `ValidationPipe` 331 - - **Exception filters** -- custom HTTP exception handling 332 - - **NestJS-specific ESLint** -- `@darraghor/eslint-plugin-nestjs-typed` 333 - - **Two controllers** -- pets and store, demonstrating per-tag grouping 334 - - **Integration tests** -- using `@nestjs/testing` + supertest 335 - 336 - --- 337 - 338 - ## Documentation 339 - 340 - Located at `docs/openapi-ts/plugins/nest.md` (~155 lines): 341 - 342 - 1. **Title + Beta Warning** 343 - 2. **About** -- "NestJS is a progressive Node.js framework..." 344 - 3. **Features** -- type-safe controller methods, tag-based grouping, incremental adoption, zero runtime coupling 345 - 4. **Installation** -- config snippet with `'nestjs'` plugin 346 - 5. **Output** -- generated per-tag types with code example 347 - 6. **Usage** -- `implements Pick<ControllerMethods, ...>` pattern 348 - 7. **Production Example** -- link to example app 349 - 8. **Constraints** -- whole-object parameter style, `@Res()` incompatibility 350 - 9. **API** -- link to UserConfig type 351 - 352 - --- 353 - 354 - ## Roadmap 355 - 356 - ### MVP (this PR) -- Done 357 - 358 - - [x] Plugin files: `plugin.ts`, `types.ts`, `config.ts`, `index.ts` 359 - - [x] Plugin registration in `config.ts` and `index.ts` 360 - - [x] Per-tag `{Tag}ControllerMethods` type alias generation 361 - - [x] Method signatures with typed path, query, body, headers params 362 - - [x] Required-before-optional parameter sorting 363 - - [x] `Promise<Response>` return types (with `Promise<void>` fallback) 364 - - [x] Example project at `examples/openapi-ts-nestjs/` 365 - - [x] Documentation at `docs/openapi-ts/plugins/nest.md` 366 - - [x] Test snapshots for OpenAPI 2.0, 3.0, and 3.1 specs 367 - 368 - ### v2 -- Decorator & Validation Support 369 - 370 - - [ ] `@ApiResponse()` / `@ApiOperation()` decorator generation 371 - - [ ] DTO class generation with `class-validator` decorators 372 - - [ ] Service interface generation 373 - - [ ] Module scaffolding option 374 - 375 - --- 376 - 377 - ## Edge Cases Handled 378 - 379 - | Edge Case | Handling | Source | 380 - | --------------------------- | ------------------------------------------------------------------ | ------------------------------------ | 381 - | No operationId | System auto-generates from method+path (e.g., `getPetsByPetId`) | Already handled by `operationToId()` | 382 - | Duplicate operation IDs | Numeric suffix appended (`result2`, `result3`) | Already handled by `operationToId()` | 383 - | Invalid TS identifier chars | Sanitized by `sanitizeNamespaceIdentifier()` | Already handled | 384 - | No parameters | Method has no params: `getInventory: () => Promise<...>` | Natural | 385 - | No response body (204) | Returns `Promise<void>` | Handled via fallback | 386 - | No tags | Grouped under `DefaultControllerMethods` | Handled | 387 - | Multiple tags | First tag wins | Handled | 388 - | File uploads (multipart) | Body type is generated but may not match `@UploadedFile()` pattern | Document as limitation | 389 - | SSE/streaming | Return type won't match `Observable<MessageEvent>` | Document as limitation | 390 - | `@Res()` usage | Extra param breaks `implements` assignability | Document as limitation | 391 - 392 - --- 393 - 394 - ## Resolved Open Questions 395 - 396 - 1. **`interface` vs `type` in DSL** -- `$.type.alias()` produces a `type` alias. Both `type` and `interface` work with `implements` in TypeScript, so this is fine. 397 - 398 - 2. **Response type symbol** -- `role: 'response'` (singular) returns the flattened union of success response bodies. Combined with `operationResponsesMap()` check to verify responses exist before referencing the symbol. 399 - 400 - 3. **`$.type.func()` parameter syntax** -- Uses `funcType.param(key, (p) => p.required(...).type(...))` pattern. Verified by examining DSL usage in other plugins.
-260
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 method types 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) -- Resolved 34 - 35 - **1. Output Format** 36 - 37 - - **Decided**: Per-tag `type` aliases (e.g., `PetsControllerMethods`, `StoreControllerMethods`) 38 - - Rationale: Tag-based grouping maps naturally to NestJS's one-controller-per-resource pattern. `type` aliases work with `implements` in TypeScript just like `interface`. 39 - 40 - **2. Method Mapping** 41 - 42 - - Map operation IDs to camelCase method names (matches SDK functions) 43 - - Example: `GET /pets` with operationId `listPets` → `listPets()` method 44 - 45 - **3. Type Structure** 46 - Request parameters: 47 - 48 - - Path params: `operation.parameters.path` → method param type 49 - - Query params: `operation.parameters.query` → method param type 50 - - Request body: `operation.body` → method param type 51 - - Headers: `operation.parameters.header` → method param type 52 - - **Parameter ordering**: Required params sorted before optional params 53 - 54 - Response: 55 - 56 - - Success responses: `operation.responses` → `Promise<OperationResponse>` return type (union of success bodies) 57 - - Error responses: Excluded — NestJS handles errors via exception filters 58 - - No response body (204): Returns `Promise<void>` 59 - 60 - **4. Platform Support** 61 - 62 - - NestJS supports both Express and Fastify adapters 63 - - Generated types are adapter-agnostic 64 - - Runtime decorators handle platform differences 65 - 66 - ### Medium Priority (Future) 67 - 68 - **5. @nestjs/swagger Integration** 69 - 70 - - Current: Generate standalone types 71 - - Future: Optionally generate `@ApiResponse()`, `@ApiOperation()` decorators 72 - - Requires config flag: `generateDecorators?: boolean` 73 - 74 - **6. Validation** 75 - 76 - - Current: Type-only validation via TypeScript 77 - - Future: Generate `class-validator` decorators in DTOs 78 - - Requires config flag: `generateValidation?: boolean` 79 - 80 - **7. Module Generation** 81 - 82 - - Current: Generate controller method types only 83 - - Future: Generate full `@Module()` setup 84 - - Requires config option: `moduleGeneration?: 'type' | 'class' | 'module'` 85 - 86 - **8. DI Layer** 87 - 88 - - Current: Controller method types only 89 - - Future: Generate service interfaces for business logic separation 90 - - Note: `ServiceMethods` was prototyped and removed — added complexity without clear value for MVP 91 - 92 - ### Low Priority (v2+) 93 - 94 - 9. Guards/interceptors/pipes type generation 95 - 10. Exception filters for typed error responses 96 - 11. SwaggerModule auto-configuration 97 - 12. Validation pipe integration 98 - 99 - ## Implemented Output 100 - 101 - ### Generated Type Aliases 102 - 103 - ```typescript 104 - // client/nestjs.gen.ts 105 - import type { 106 - CreatePetData, 107 - CreatePetResponse, 108 - DeletePetData, 109 - DeletePetResponse, 110 - ListPetsData, 111 - ListPetsResponse, 112 - ShowPetByIdData, 113 - ShowPetByIdResponse, 114 - UpdatePetData, 115 - UpdatePetResponse, 116 - } from './types.gen'; 117 - 118 - export type PetsControllerMethods = { 119 - createPet: (body: CreatePetData['body']) => Promise<CreatePetResponse>; 120 - deletePet: (path: DeletePetData['path']) => Promise<DeletePetResponse>; 121 - listPets: (query?: ListPetsData['query']) => Promise<ListPetsResponse>; 122 - showPetById: (path: ShowPetByIdData['path']) => Promise<ShowPetByIdResponse>; 123 - updatePet: ( 124 - path: UpdatePetData['path'], 125 - body: UpdatePetData['body'], 126 - ) => Promise<UpdatePetResponse>; 127 - }; 128 - 129 - export type StoreControllerMethods = { 130 - getInventory: () => Promise<GetInventoryResponse>; 131 - }; 132 - ``` 133 - 134 - ### Usage Pattern 135 - 136 - ```typescript 137 - // controllers/pets.controller.ts 138 - import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common'; 139 - import type { PetsControllerMethods } from '../client/nestjs.gen'; 140 - 141 - @Controller('pets') 142 - export class PetsController implements Pick< 143 - PetsControllerMethods, 144 - 'listPets' | 'createPet' | 'showPetById' 145 - > { 146 - @Get() 147 - async listPets(@Query() query?: ListPetsData['query']) { 148 - return []; 149 - } 150 - 151 - @Post() 152 - async createPet(@Body() body: CreatePetDto) { 153 - return { id: 1 }; 154 - } 155 - 156 - @Get(':petId') 157 - async showPetById(@Param() path: ShowPetByIdData['path']) { 158 - return { id: Number(path.petId), name: 'Kitty' }; 159 - } 160 - } 161 - ``` 162 - 163 - ## Example Structure 164 - 165 - ``` 166 - examples/openapi-ts-nestjs/ 167 - ├── openapi-ts.config.ts # plugins: ['nestjs', '@hey-api/sdk'] 168 - ├── openapi.json # Petstore spec (extended with update/delete) 169 - ├── eslint.config.js # NestJS-specific linting 170 - ├── package.json # @nestjs/core, @nestjs/common, @nestjs/platform-express 171 - ├── src/ 172 - │ ├── client/ 173 - │ │ ├── nestjs.gen.ts # Generated controller method types 174 - │ │ ├── types.gen.ts # Shared types (from TypeScript plugin) 175 - │ │ ├── sdk.gen.ts # Client SDK (from SDK plugin) 176 - │ │ └── index.ts 177 - │ ├── pets/ 178 - │ │ ├── pets.controller.ts # Implements PetsControllerMethods 179 - │ │ └── pets.module.ts 180 - │ ├── store/ 181 - │ │ ├── store.controller.ts # Implements StoreControllerMethods 182 - │ │ └── store.module.ts 183 - │ ├── common/ 184 - │ │ └── filters/ 185 - │ │ └── http-exception.filter.ts 186 - │ ├── app.module.ts # NestJS module setup 187 - │ └── main.ts # Bootstrap app 188 - └── test/ 189 - └── pets.test.ts # Integration tests 190 - ``` 191 - 192 - ## Dependencies 193 - 194 - ### Plugin (dev) 195 - 196 - - `@hey-api/typescript` - Base type generation (required dependency) 197 - 198 - ### Example (runtime) 199 - 200 - - `@nestjs/core` (^10.0.0) 201 - - `@nestjs/common` (^10.0.0) 202 - - `@nestjs/platform-express` (^10.0.0) or `@nestjs/platform-fastify` 203 - - `@nestjs/swagger` - OpenAPI documentation 204 - - `class-validator` - DTO request validation 205 - - `class-transformer` - DTO transformation 206 - 207 - ## Implementation Approach 208 - 209 - 1. **Plugin structure** at `packages/openapi-ts/src/plugins/nestjs/`: 210 - - `plugin.ts` (~143 lines) - Core logic 211 - - `types.ts` (5 lines) - Plugin type definitions 212 - - `config.ts` (18 lines) - Plugin registration 213 - - `index.ts` (2 lines) - Re-exports 214 - 215 - 2. **Type generation** using per-tag grouping: 216 - - Iterate operations via `plugin.forEach('operation')` 217 - - Group by first OpenAPI tag (default tag: `'default'`) 218 - - For each operation: extract params, build method signature 219 - - Emit per-tag `type` alias: `{PascalTag}ControllerMethods` 220 - 221 - 3. **Parameter mapping** with required-before-optional sorting: 222 - - Collect all params (path, query, body, headers) 223 - - Sort: required params first, optional params last 224 - - Emit as individual function parameters 225 - 226 - 4. **Response type mapping**: 227 - - Use `operationResponsesMap()` to get response info 228 - - Query `role: 'response'` symbol (union of success bodies, not status-code map) 229 - - Wrap in `Promise<T>`, fallback to `Promise<void>` 230 - 231 - 5. **Registration**: 232 - - Added to `packages/openapi-ts/src/plugins/config.ts` 233 - - Added to `packages/openapi-ts/src/index.ts` PluginConfigMap 234 - 235 - ## Deferred Features (v2+) 236 - 237 - - Full `@ApiResponse()`, `@ApiOperation()` decorator generation 238 - - `class-validator` decorators in generated DTOs 239 - - Full module generation with `@Module()` decorator 240 - - Service interface generation for DI layer (prototyped and removed) 241 - - Guard/interceptor/pipe type generation 242 - - Exception filter types 243 - - SwaggerModule auto-configuration 244 - 245 - ## Critical Files Reference 246 - 247 - **Plugin implementation:** 248 - 249 - - `packages/openapi-ts/src/plugins/nestjs/plugin.ts` - Core logic (143 lines) 250 - - `packages/openapi-ts/src/plugins/nestjs/types.ts` - UserConfig 251 - - `packages/openapi-ts/src/plugins/nestjs/config.ts` - Plugin registration 252 - - `packages/openapi-ts/src/plugins/nestjs/index.ts` - Re-exports 253 - 254 - **Studied for implementation:** 255 - 256 - - `packages/openapi-ts/src/plugins/fastify/plugin.ts` - Core logic template 257 - - `packages/openapi-ts/src/plugins/fastify/types.ts` - UserConfig pattern 258 - - `packages/openapi-ts/src/plugins/fastify/config.ts` - Plugin registration 259 - - `examples/openapi-ts-fastify/src/handlers.ts` - Handler pattern 260 - - `docs/openapi-ts/plugins/fastify.md` - Docs template
+51
packages/openapi-ts/src/plugins/nestjs/README.md
··· 1 + # NestJS Plugin 2 + 3 + Generate per-tag `ControllerMethods` type aliases from OpenAPI specs for NestJS controllers. 4 + 5 + ## Design Decisions 6 + 7 + | Decision | Choice | Rationale | 8 + | --------------------- | ---------------------------------------------- | ----------------------------------------------------------- | 9 + | Output format | Per-tag `{Tag}ControllerMethods` type aliases | Maps 1:1 to NestJS one-controller-per-resource pattern | 10 + | `type` vs `interface` | `type` alias | DSL produces `type` aliases; both work with `implements` | 11 + | Parameters | Individual: `path`, `query`, `body`, `headers` | Maps 1:1 to `@Param()`, `@Query()`, `@Body()`, `@Headers()` | 12 + | Parameter ordering | Required before optional | Prevents TS error when optional precedes required | 13 + | Return type | `Promise<OperationResponse>` | Controllers return values, not status-code maps | 14 + | Config options | None | Zero config = fastest path to value | 15 + | Untagged operations | `DefaultControllerMethods` | Consistent fallback | 16 + | `includeInEntry` | `false` | Users import from `nestjs.gen.ts` directly | 17 + 18 + ## Constraints 19 + 20 + **`implements` requires whole-object parameter style:** 21 + 22 + ```typescript 23 + // WORKS — whole-object style 24 + async showPetById(@Param() path: ShowPetByIdData['path']) { ... } 25 + 26 + // BREAKS — decomposed style (different signature than generated type) 27 + async showPetById(@Param('petId') petId: string) { ... } 28 + ``` 29 + 30 + **Incompatible patterns:** 31 + 32 + - `@Res()` — extra parameter breaks assignability 33 + - `@UploadedFile()` — bypass `implements` for file upload methods 34 + - SSE endpoints — `Observable<MessageEvent>` doesn't match `Promise<T>` 35 + 36 + **Not generated:** 37 + 38 + - Cookie parameters — NestJS handles cookies via `@Req()` or cookie middleware, not method params 39 + - Error responses — NestJS handles errors via exception filters 40 + 41 + ## Edge Cases 42 + 43 + | Case | Handling | 44 + | ----------------------- | -------------------------------------------- | 45 + | No operationId | Auto-generated by `operationToId()` | 46 + | Duplicate operation IDs | Numeric suffix (`result2`, `result3`) | 47 + | Invalid TS identifiers | Sanitized by `sanitizeNamespaceIdentifier()` | 48 + | No parameters | `() => Promise<T>` | 49 + | No response body (204) | `Promise<void>` | 50 + | Multiple tags | First tag wins | 51 + | Non-ASCII in tags/names | Handled |