@@ -16,32 +16,68 @@ import {
1616} from '@nestjs/swagger' ;
1717import { RedisAuthGuard } from '../../auth/redis-auth.guard' ;
1818
19+ /**
20+ * API Decorator Response Type
21+ * Alias for Swagger's ApiResponseOptions to simplify type usage
22+ */
1923export type ApiDecoratorResponse = ApiResponseOptions ;
2024
25+ /**
26+ * API Decorator Options Interface
27+ *
28+ * This interface defines all available options for the @Api() decorator,
29+ * which provides a unified way to configure Swagger documentation for NestJS endpoints.
30+ *
31+ * @example
32+ * ```typescript
33+ * @Api ({
34+ * summary: 'Get all users',
35+ * description: 'Returns a paginated list of users',
36+ * paginatedResponseType: UserDto,
37+ * queriesFrom: [PaginationArgs, UserFilterDto],
38+ * authenticationRequired: true,
39+ * envelope: true
40+ * })
41+ * ```
42+ */
2143export interface ApiOptions {
2244 /** Shorthand for ApiOperation -> summary */
2345 summary ?: string ;
46+
2447 /** Shorthand for ApiOperation -> description */
2548 description ?: string ;
49+
2650 /** Full ApiOperation options (overrides summary/description if provided) */
2751 apiOperationOptions ?: Partial < ApiOperationOptions > ;
52+
2853 /** Explicit list of responses. If provided, default Created/Unauthorized/Forbidden set is suppressed (except Unauthorized when auth required). */
2954 responses ?: ApiDecoratorResponse [ ] ;
30- /** Backwards compat (deprecated) */
55+
56+ /** @deprecated Backwards compatibility - use 'responses' instead */
3157 apiResponses ?: Partial < ApiResponseOptions > [ ] ;
32- /** When true attaches RedisAuthGuard + bearer auth + 401 response (if not explicitly supplied). */
58+
59+ /** When true, attaches RedisAuthGuard + bearer auth + 401 response (if not explicitly supplied). */
3360 authenticationRequired ?: boolean ;
34- /** DTO / class for request body */
61+
62+ /** DTO / class for request body - will be automatically inferred from method signature if not provided */
3563 bodyType ?: NestType < any > | ( new ( ...args : any [ ] ) => any ) ;
36- /** Path params */
64+
65+ /**
66+ * Manually defined path parameters
67+ * For automatic path param extraction, use 'pathParamsFrom' instead
68+ */
3769 params ?: Array < {
3870 name : string ;
3971 description ?: string ;
4072 type ?: any ;
4173 required ?: boolean ;
4274 enum ?: any [ ] ;
4375 } > ;
44- /** Query params */
76+
77+ /**
78+ * Manually defined query parameters
79+ * For automatic query param extraction, use 'queriesFrom' instead
80+ */
4581 queries ?: Array < {
4682 name : string ;
4783 description ?: string ;
@@ -50,25 +86,88 @@ export interface ApiOptions {
5086 enum ?: any [ ] ;
5187 example ?: any ;
5288 } > ;
53- /** Derive query params from one or more DTO / classes with Field decorators */
89+
90+ /**
91+ * Automatically derive query params from one or more DTOs with @Field decorators
92+ * The decorator will read metadata stored by @Field decorators (where inQuery: true)
93+ * and generate @ApiQuery decorators for Swagger documentation
94+ *
95+ * @example
96+ * queriesFrom: [PaginationArgs, JobFilterDto]
97+ */
5498 queriesFrom ?: ( new ( ...args : any [ ] ) => any ) | Array < new ( ...args : any [ ] ) => any > ;
55- /** Derive path params from one or more DTO / classes with Field decorators */
99+
100+ /**
101+ * Automatically derive path params from one or more DTOs with @Field decorators
102+ * The decorator will read metadata stored by @Field decorators (where inPath: true)
103+ * and generate @ApiParam decorators for Swagger documentation
104+ *
105+ * @example
106+ * pathParamsFrom: JobIdPathParamsDto
107+ */
56108 pathParamsFrom ?: ( new ( ...args : any [ ] ) => any ) | Array < new ( ...args : any [ ] ) => any > ;
57- /** Mark operation deprecated */
109+
110+ /** Mark operation deprecated in Swagger UI */
58111 deprecated ?: boolean ;
112+
59113 /** Shorthand to specify a single 200 response type */
60114 responseType ?: NestType < any > ;
115+
61116 /** Shorthand to specify an array 200 response type */
62117 responseArrayType ?: NestType < any > ;
63- /** Shorthand to specify a paginated 200 response type (items + pageInfo) */
118+
119+ /**
120+ * Shorthand to specify a paginated 200 response type
121+ * Generates a schema with { items: [], pageInfo: {}, totalCount: number, meta?: {} }
122+ */
64123 paginatedResponseType ?: NestType < any > ;
65- /** Wrap successful 2xx response in a standard envelope { success, data, error? } */
124+
125+ /**
126+ * When true, wraps successful 2xx response in a standard envelope: { success, data, error? }
127+ * This metadata is also stored for use by interceptors
128+ */
66129 envelope ?: boolean ;
67130}
68131
132+ /**
133+ * @Api Decorator
134+ *
135+ * A powerful unified decorator for configuring Swagger/OpenAPI documentation in NestJS.
136+ * Combines multiple Swagger decorators into a single, declarative interface.
137+ *
138+ * Key Features:
139+ * - Automatic request body type inference from method signatures
140+ * - Auto-generation of query/path params from DTOs with @Field decorators
141+ * - Support for paginated, array, and enveloped responses
142+ * - Built-in authentication guard integration
143+ * - Flexible response configuration
144+ *
145+ * @param options - Configuration options for the API endpoint
146+ * @returns A method decorator that applies all necessary Swagger decorators
147+ *
148+ * @example
149+ * ```typescript
150+ * @Get ()
151+ * @Api ({
152+ * summary: 'Get all jobs',
153+ * description: 'Returns a paginated list of jobs with optional filters',
154+ * paginatedResponseType: JobDto,
155+ * queriesFrom: [PaginationArgs, JobFilterDto],
156+ * envelope: true
157+ * })
158+ * async findAll(@Query() pagination: PaginationArgs, @Query() filters: JobFilterDto) {
159+ * // ...
160+ * }
161+ * ```
162+ */
69163export function Api ( options : ApiOptions ) : MethodDecorator {
70164 return function ( target : any , propertyKey : string | symbol , descriptor : PropertyDescriptor ) {
71- // Attempt body type inference if not supplied
165+ // ============================================================================
166+ // STEP 1: Automatic Body Type Inference
167+ // ============================================================================
168+ // Attempt to infer the request body type from the method's parameter types
169+ // if it hasn't been explicitly provided in the options.
170+ // This looks for DTO or Input classes in the method signature.
72171 if ( ! options . bodyType ) {
73172 try {
74173 const paramTypes : any [ ] = Reflect . getMetadata ( 'design:paramtypes' , target , propertyKey ) || [ ] ;
@@ -85,29 +184,49 @@ export function Api(options: ApiOptions): MethodDecorator {
85184 }
86185 }
87186
187+ // ============================================================================
188+ // STEP 2: Build API Operation Metadata
189+ // ============================================================================
190+ // Construct the ApiOperation options with summary, description, and deprecation info
88191 const op : ApiOperationOptions = {
89192 summary : options . summary ,
90193 description : options . description ,
91194 deprecated : options . deprecated ,
92195 ...( options . apiOperationOptions || { } ) ,
93196 } as ApiOperationOptions ;
94197
198+ // ============================================================================
199+ // STEP 3: Determine Response Strategy
200+ // ============================================================================
201+ // Check if the user has provided custom responses.
202+ // If they have, we skip adding default responses (Created, Unauthorized, Forbidden)
95203 const userProvidedResponses = ( options . responses || options . apiResponses || [ ] ) . filter ( ( v ) =>
96204 Boolean ( v )
97205 ) as ApiResponseOptions [ ] ;
98206 const addDefaultSet = userProvidedResponses . length === 0 ; // Only add defaults when no custom responses passed
99207
208+ // ============================================================================
209+ // STEP 4: Build Decorator Chain
210+ // ============================================================================
211+ // Start building the array of decorators that will be applied to the method
100212 const decorators : any [ ] = [ ] ;
213+
214+ // Add authentication guards if required
101215 if ( options . authenticationRequired ) {
102216 decorators . push ( UseGuards ( RedisAuthGuard ) , ApiBearerAuth ( ) ) ;
103217 }
218+
104219 decorators . push ( ApiOperation ( op ) ) ;
105220
221+ // Add request body documentation if bodyType is specified
106222 if ( options . bodyType ) {
107223 decorators . push ( ApiBody ( { type : options . bodyType } ) ) ;
108224 }
109225
110- // Params
226+ // ============================================================================
227+ // STEP 5: Path Parameters
228+ // ============================================================================
229+ // Add manually defined path parameters
111230 ( options . params || [ ] ) . forEach ( ( p ) =>
112231 decorators . push (
113232 ApiParam ( {
@@ -120,7 +239,8 @@ export function Api(options: ApiOptions): MethodDecorator {
120239 )
121240 ) ;
122241
123- // Auto path params from metadata
242+ // Auto-generate path parameters from DTOs with @Field decorators
243+ // Reads metadata set by @Field (... inPath: true) decorators
124244 if ( options . pathParamsFrom ) {
125245 const sources = Array . isArray ( options . pathParamsFrom ) ? options . pathParamsFrom : [ options . pathParamsFrom ] ;
126246 sources . forEach ( ( src ) => {
@@ -142,7 +262,11 @@ export function Api(options: ApiOptions): MethodDecorator {
142262 } ) ;
143263 }
144264
145- // Queries
265+ // ============================================================================
266+ // STEP 6: Query Parameters
267+ // ============================================================================
268+ // Add manually defined query parameters
269+ // Note: For manual queries, required defaults to false (must be explicitly true)
146270 ( options . queries || [ ] ) . forEach ( ( q ) =>
147271 decorators . push (
148272 ApiQuery ( {
@@ -156,10 +280,15 @@ export function Api(options: ApiOptions): MethodDecorator {
156280 )
157281 ) ;
158282
283+ // Auto-generate query parameters from DTOs with @Field decorators
284+ // Reads metadata set by @Field (... inQuery: true) decorators
285+ // IMPORTANT: We explicitly set required to true or false (not undefined)
286+ // to ensure Swagger correctly displays optional fields
159287 if ( options . queriesFrom ) {
160288 const sources = Array . isArray ( options . queriesFrom ) ? options . queriesFrom : [ options . queriesFrom ] ;
161289 sources . forEach ( ( src ) => {
162290 if ( ! src ) return ;
291+ // Retrieve metadata stored by @Field decorator
163292 const qMeta = Reflect . getMetadata ( 'cb:fieldMeta' , src . prototype ) || [ ] ;
164293 qMeta
165294 . filter ( ( m : any ) => m . inQuery )
@@ -168,7 +297,9 @@ export function Api(options: ApiOptions): MethodDecorator {
168297 ApiQuery ( {
169298 name : m . name ,
170299 description : m . description ,
171- required : m . required === true ,
300+ // Explicitly set required to true or false (never undefined)
301+ // This ensures Swagger properly marks optional fields as not required
302+ required : m . required === true ? true : false ,
172303 enum : m . enum ,
173304 type : m . type ,
174305 } )
@@ -177,11 +308,17 @@ export function Api(options: ApiOptions): MethodDecorator {
177308 } ) ;
178309 }
179310
311+ // ============================================================================
312+ // STEP 7: Response Documentation
313+ // ============================================================================
314+ // Generate response schemas based on the provided options
180315 // Shorthand 200 response helpers (only if user didn't explicitly define 200)
181316 const hasExplicit200 = userProvidedResponses . some ( ( r ) => r . status === 200 ) ;
182317 if ( ! hasExplicit200 ) {
318+ // Single object response
183319 if ( options . responseType ) {
184320 if ( options . envelope ) {
321+ // Wrapped in envelope: { success: true, data: {...} }
185322 decorators . push (
186323 ApiResponse ( {
187324 status : 200 ,
@@ -196,14 +333,18 @@ export function Api(options: ApiOptions): MethodDecorator {
196333 } )
197334 ) ;
198335 } else {
336+ // Direct response without envelope
199337 decorators . push ( ApiResponse ( { status : 200 , description : 'Successful response' , type : options . responseType } ) ) ;
200338 }
201- } else if ( options . responseArrayType ) {
339+ }
340+ // Array response
341+ else if ( options . responseArrayType ) {
202342 const arraySchema = {
203343 type : 'array' ,
204344 items : { $ref : getSchemaPath ( options . responseArrayType ) } ,
205345 } ;
206346 if ( options . envelope ) {
347+ // Wrapped in envelope: { success: true, data: [...] }
207348 decorators . push (
208349 ApiResponse ( {
209350 status : 200 ,
@@ -218,6 +359,7 @@ export function Api(options: ApiOptions): MethodDecorator {
218359 } )
219360 ) ;
220361 } else {
362+ // Direct array response
221363 decorators . push (
222364 ApiResponse ( {
223365 status : 200 ,
@@ -226,7 +368,9 @@ export function Api(options: ApiOptions): MethodDecorator {
226368 } )
227369 ) ;
228370 }
229- } else if ( options . paginatedResponseType ) {
371+ }
372+ // Paginated response with items, pageInfo, totalCount, and optional meta
373+ else if ( options . paginatedResponseType ) {
230374 const basePaginated = {
231375 type : 'object' ,
232376 properties : {
@@ -253,6 +397,7 @@ export function Api(options: ApiOptions): MethodDecorator {
253397 } ,
254398 } ;
255399 if ( options . envelope ) {
400+ // Wrapped in envelope: { success: true, data: { items, pageInfo, totalCount, meta } }
256401 decorators . push (
257402 ApiResponse ( {
258403 status : 200 ,
@@ -267,6 +412,7 @@ export function Api(options: ApiOptions): MethodDecorator {
267412 } )
268413 ) ;
269414 } else {
415+ // Direct paginated response
270416 decorators . push (
271417 ApiResponse ( {
272418 status : 200 ,
@@ -278,23 +424,35 @@ export function Api(options: ApiOptions): MethodDecorator {
278424 }
279425 }
280426
427+ // ============================================================================
428+ // STEP 8: Default Error Responses
429+ // ============================================================================
430+ // Add default error responses (401, 201, 403) if user hasn't provided custom responses
281431 if ( addDefaultSet ) {
282432 decorators . push ( ApiUnauthorizedResponse ( { description : 'Unauthorized' } ) ) ;
283433 decorators . push ( ApiCreatedResponse ( { description : 'The record has been successfully created.' } ) ) ;
284434 decorators . push ( ApiForbiddenResponse ( { description : 'Forbidden.' } ) ) ;
285435 } else {
436+ // If user provided custom responses, ensure 401 is still added for authenticated endpoints
286437 let has401 = userProvidedResponses . some ( ( r ) => r . status === 401 ) ;
287438 if ( options . authenticationRequired && ! has401 ) {
288439 decorators . push ( ApiUnauthorizedResponse ( { description : 'Unauthorized' } ) ) ;
289440 has401 = true ;
290441 }
291442 }
292443
444+ // ============================================================================
445+ // STEP 9: Add User-Provided Responses
446+ // ============================================================================
447+ // Add any custom responses provided by the user
293448 if ( userProvidedResponses . length > 0 ) {
294449 userProvidedResponses . forEach ( ( r ) => decorators . push ( ApiResponse ( r ) ) ) ;
295450 }
296451
297- // Store envelope intention for interceptor usage
452+ // ============================================================================
453+ // STEP 10: Store Metadata for Interceptors
454+ // ============================================================================
455+ // Store envelope metadata so interceptors can wrap responses appropriately
298456 if ( options . envelope ) {
299457 try {
300458 Reflect . defineMetadata ( 'cb:envelope' , true , descriptor . value ) ;
@@ -303,6 +461,10 @@ export function Api(options: ApiOptions): MethodDecorator {
303461 }
304462 }
305463
464+ // ============================================================================
465+ // STEP 11: Apply All Decorators
466+ // ============================================================================
467+ // Apply all collected decorators to the target method
306468 applyDecorators ( ...decorators ) ( target , propertyKey , descriptor ) ;
307469 } ;
308470}
0 commit comments