Skip to content

Commit c994c97

Browse files
Merge branch 'feature/swagger-common-pattern' of https://github.com/nartc/docs.nestjs.com into nartc-feature/swagger-common-pattern
2 parents 3d048a7 + cd37681 commit c994c97

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

content/openapi/operations.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,177 @@ 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: Generics ApiResponse
157+
158+
With the ability to provide **Raw Definition**, we can provide **Generics** schema for SwaggerUI. Assume we have the following *generics 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` because we will be providing **Raw Definition** for it later. Now, let's assume we have the following `CatDto`
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+
Now, we can start providing a `PaginatedDto<CatDto>` on `CatController`
191+
192+
```ts
193+
@Controller(...)
194+
export class CatController {
195+
196+
@Get()
197+
@ApiOkResponse({
198+
schema: {
199+
allOf: [
200+
{ $ref: getSchemaPath(PaginatedDto) },
201+
{
202+
properties: {
203+
results: {
204+
type: 'array',
205+
items: { $ref: getSchemaPath(CatDto) },
206+
},
207+
},
208+
},
209+
],
210+
},
211+
})
212+
async get(...): Promise<PaginatedDto<CatDto>> {
213+
...
214+
}
215+
}
216+
```
217+
218+
We are not done. `PaginatedDto` isn't part of any controller by itself so `SwaggerModule` won't be able to scan it
219+
during initialization. But, `nestjs/swagger` provides an `ApiExtraModels()` decorator for such cases.
220+
221+
```ts
222+
@Controller(...)
223+
@ApiExtraModels(PaginatedDto)
224+
export class CatController {
225+
...
226+
}
227+
```
228+
229+
> info **Hint** You only need to use `ApiExtraModels` for a specific `Dto` once so find a place where it makes sense for you to do so.
230+
231+
- `getSchemaPath()` returns the OpenAPI Schema path from within the OpenAPI Spec File that `ApiExtraModels` helps `nestjs/swagger` generates,
232+
or `nestjs/swagger` is able to scan automatically.
233+
- `allOf` is a concept that OpenAPI 3 has to cover Inheritance use-cases.
234+
235+
In this case, we tell SwaggerUI that this response will have **allOf** `PaginatedDto` and the `results` property will be of type array and each item will be of type `CatDto`.
236+
If you run the SwaggerUI now, you'd see the generated `swagger.json` for this specific endpoint like the following:
237+
238+
```json
239+
responses": {
240+
"200": {
241+
"description": "",
242+
"content": {
243+
"application/json": {
244+
"schema": {
245+
"allOf": [
246+
{
247+
"$ref": "#/components/schemas/PaginatedDto"
248+
},
249+
{
250+
"properties": {
251+
"results": {
252+
"$ref": "#/components/schemas/CatDto"
253+
}
254+
}
255+
}
256+
]
257+
}
258+
}
259+
}
260+
}
261+
}
262+
```
263+
264+
Now that we know it works, we can create a custom decorator for `PaginatedDto` as follow:
265+
266+
```ts
267+
export const ApiPaginatedResponse = <TModel extends Type<any>>(model: TModel) => {
268+
return applyDecorators(
269+
ApiOkResponse({
270+
schema: {
271+
allOf: [
272+
{ $ref: getSchemaPath(PaginatedDto) },
273+
{
274+
properties: {
275+
results: {
276+
type: 'array',
277+
items: { $ref: getSchemaPath(model) },
278+
},
279+
},
280+
},
281+
],
282+
},
283+
}),
284+
);
285+
};
286+
```
287+
288+
then we can use `ApiPaginatedResponse` on our endpoint:
289+
290+
```ts
291+
@Get()
292+
@ApiPaginatedResponse(CatDto)
293+
async get(): Promise<PaginatedDto<CatDto>> {}
294+
```
295+
296+
You can modify `ApiPaginatedResponse` as you see fit, maybe make it more generics to handle non-array `results` or maybe different property name than `results`.
297+
Knowing the capabilities of `nestjs/swagger` APIs, you can totally go wild with it and make sure your OpenAPI Spec is correct and covered.
298+
299+
For client generation tools, this approach poses an ambiguity in how this `PaginatedResponse<TModel>` is being generated for the client. The following snippet is an example of a client generator result of the above **GET** request:
300+
301+
```ts
302+
// Angular
303+
get(): Observable<{ total: number, limit: number, offset: number, results: CatDto[] }>
304+
```
305+
306+
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`:
307+
308+
```ts
309+
export const ApiPaginatedResponse = <TModel extends Type<any>>(model: TModel) => {
310+
return applyDecorators(
311+
ApiOkResponse({
312+
schema: {
313+
title: `PaginatedResponseOf${model.name}`
314+
allOf: [
315+
// ...
316+
],
317+
},
318+
}),
319+
);
320+
};
321+
```
322+
323+
Now the result of the client generator tool will become:
324+
325+
```ts
326+
// Angular
327+
get(): Observable<PaginatedResponseOfCatDto>
328+
```

0 commit comments

Comments
 (0)