Skip to content

Commit 26cacb6

Browse files
Merge branch 'nartc-feature/swagger-common-pattern'
2 parents 3d048a7 + 0708da0 commit 26cacb6

File tree

2 files changed

+174
-2
lines changed

2 files changed

+174
-2
lines changed

content/openapi/operations.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,165 @@ To add an Extension to a request use the `@ApiExtension()` decorator. The extens
152152
```typescript
153153
@ApiExtension('x-foo', { hello: 'world' })
154154
```
155+
156+
#### Advanced: Generic `ApiResponse`
157+
158+
With the ability to provide [Raw Definitions](/openapi/types-and-parameters#raw-definitions), we can define Generic schema for Swagger UI. Assume we have the following DTO:
159+
160+
```ts
161+
export class PaginatedDto<TData> {
162+
@ApiProperty()
163+
total: number;
164+
165+
@ApiProperty()
166+
limit: number;
167+
168+
@ApiProperty()
169+
offset: number;
170+
171+
results: TData[];
172+
}
173+
```
174+
175+
We skip decorating `results` as we will be providing a raw definition for it later. Now, let's define another DTO and name it, for example, `CatDto`, as follows:
176+
177+
```ts
178+
export class CatDto {
179+
@ApiProperty()
180+
name: string;
181+
182+
@ApiProperty()
183+
age: number;
184+
185+
@ApiProperty()
186+
breed: string;
187+
}
188+
```
189+
190+
With this in place, we can define a `PaginatedDto<CatDto>` response, as follows:
191+
192+
```ts
193+
@ApiOkResponse({
194+
schema: {
195+
allOf: [
196+
{ $ref: getSchemaPath(PaginatedDto) },
197+
{
198+
properties: {
199+
results: {
200+
type: 'array',
201+
items: { $ref: getSchemaPath(CatDto) },
202+
},
203+
},
204+
},
205+
],
206+
},
207+
})
208+
async findAll(): Promise<PaginatedDto<CatDto>> {}
209+
```
210+
211+
In this example, we specify that the response will have allOf `PaginatedDto` and the `results` property will be of type `Array<CatDto>`.
212+
213+
- `getSchemaPath()` function that returns the OpenAPI Schema path from within the OpenAPI Spec File for a given model.
214+
- `allOf` is a concept that OAS 3 provides to cover various Inheritance related use-cases.
215+
216+
Lastly, since `PaginatedDto` is not directly referenced by any controller, the `SwaggerModule` will not be able to generate a corresponding model definition just yet. In this case, we must add it as an [Extra Model](/openapi/types-and-parameters#extra-models). For example, we can use the `@ApiExtraModels()` decorator on the controller level, as follows:
217+
218+
```ts
219+
@Controller('cats')
220+
@ApiExtraModels(PaginatedDto)
221+
export class CatsController {}
222+
```
223+
224+
If you run Swagger now, the generated `swagger.json` for this specific endpoint should have the followng response defined:
225+
226+
```json
227+
"responses": {
228+
"200": {
229+
"description": "",
230+
"content": {
231+
"application/json": {
232+
"schema": {
233+
"allOf": [
234+
{
235+
"$ref": "#/components/schemas/PaginatedDto"
236+
},
237+
{
238+
"properties": {
239+
"results": {
240+
"$ref": "#/components/schemas/CatDto"
241+
}
242+
}
243+
}
244+
]
245+
}
246+
}
247+
}
248+
}
249+
}
250+
```
251+
252+
To make it reusable, we can create a custom decorator for `PaginatedDto`, as follows:
253+
254+
```ts
255+
export const ApiPaginatedResponse = <TModel extends Type<any>>(
256+
model: TModel,
257+
) => {
258+
return applyDecorators(
259+
ApiOkResponse({
260+
schema: {
261+
allOf: [
262+
{ $ref: getSchemaPath(PaginatedDto) },
263+
{
264+
properties: {
265+
results: {
266+
type: 'array',
267+
items: { $ref: getSchemaPath(model) },
268+
},
269+
},
270+
},
271+
],
272+
},
273+
}),
274+
);
275+
};
276+
```
277+
278+
> info **Hint** `Type<any>` interface and `applyDecorators` function are imported from the `@nestjs/common` package.
279+
280+
With this in place, we can use the custom `@ApiPaginatedResponse()` decorator on our endpoint:
281+
282+
```ts
283+
@ApiPaginatedResponse(CatDto)
284+
async findAll(): Promise<PaginatedDto<CatDto>> {}
285+
```
286+
287+
For client generation tools, this approach poses an ambiguity in how the `PaginatedResponse<TModel>` is being generated for the client. The following snippet is an example of a client generator result for the above `GET /` endpoint.
288+
289+
```typescript
290+
// Angular
291+
findAll(): Observable<{ total: number, limit: number, offset: number, results: CatDto[] }>
292+
```
293+
294+
As you can see, the **Return Type** here is ambiguous. To workaround this issue, you can add a `title` property to the `schema` for `ApiPaginatedResponse`:
295+
296+
```typescript
297+
export const ApiPaginatedResponse = <TModel extends Type<any>>(model: TModel) => {
298+
return applyDecorators(
299+
ApiOkResponse({
300+
schema: {
301+
title: `PaginatedResponseOf${model.name}`
302+
allOf: [
303+
// ...
304+
],
305+
},
306+
}),
307+
);
308+
};
309+
```
310+
311+
Now the result of the client generator tool will become:
312+
313+
```ts
314+
// Angular
315+
findAll(): Observable<PaginatedResponseOfCatDto>
316+
```

content/openapi/types-and-parameters.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,24 @@ async create(@Body() coords: number[][]) {}
239239

240240
#### Extra models
241241

242-
To define additional models that should be inspected by the Swagger module, use the `@ApiExtraModels()` decorator:
242+
To define additional models that are not directly referenced in your controllers but should be inspected by the Swagger module, use the `@ApiExtraModels()` decorator:
243243

244244
```typescript
245245
@ApiExtraModels(ExtraModel)
246246
export class CreateCatDto {}
247247
```
248248

249-
Then, you can get the reference (`$ref`) to your model using `getSchemaPath(ExtraModel)`:
249+
> info **Hint** You only need to use `@ApiExtraModels()` once for a specific model class.
250+
251+
Alternatively, you can pass an options object with the `extraModels` property specified to the `SwaggerModule#createDocument()` method, as follows:
252+
253+
```typescript
254+
const document = SwaggerModule.createDocument(app, options, {
255+
extraModels: [ExtraModel],
256+
});
257+
```
258+
259+
To get a reference (`$ref`) to your model, use the `getSchemaPath(ExtraModel)` function:
250260

251261
```typescript
252262
'application/vnd.api+json': {

0 commit comments

Comments
 (0)