NestJS course Β· Module 11: Swagger and OpenAPI
Controller decorators - seals on the decrees
In this lesson5
A decree without a seal is a sheet of paper. There is no telling who issued it, what it concerns, or whether it is in force at all - and an official in the provinces must settle that before doing anything.
An endpoint without a description is exactly the same. Swagger will read the path and the HTTP method out of the code by itself, but it cannot guess what the endpoint does, which parameters are required, or what the code it returns signifies. The decorators of the @nestjs/swagger package add what the code alone does not show.
An important note at the outset: all these decorators only describe. None of them changes the application's behaviour - an endpoint without @ApiOperation works exactly as before, it is merely nameless in the documentation.
@ApiTags and @ApiOperation
The first two seals answer the questions "where do I find this" and "what does it do":
1@ApiTags('Provinces')
2@Controller('provinces')
3export class ProvincesController {
4 @ApiOperation({
5 summary: 'List of provinces',
6 description: 'Returns every province of the Empire together with its governor.',
7 })
8 @Get()
9 findAll() {
10 return this.provincesService.findAll();
11 }
12}@ApiTags() groups endpoints into sections - in Swagger UI this becomes a collapsible group. You place it above the controller class, because the grouping applies to all its methods at once. Several tags may be listed, separated by commas, when a controller belongs to two areas.
@ApiOperation() accepts two main properties: summary and description. Not url and method - NestJS reads those from @Get and @Controller. Not host and port - that is server configuration, not a description of an endpoint. And not controller or service - those are class names the documentation need know nothing about.
The difference between them is practical: summary is the single sentence shown in the endpoint list, description the longer text revealed on clicking. If you are to write only one, write the summary.
@ApiParam versus @ApiQuery
These two are the most often confused, because both describe "something arriving in the address". The place in the URL settles it:
1@ApiParam({ name: 'id', description: 'The province identifier' })
2@Get(':id')
3findOne(@Param('id') id: string) {
4 return this.provincesService.findOne(id);
5}@ApiParam() describes route parameters in the URL path - the ones written with a colon in the route, like :id, which form part of the address. In /provinces/7 the seven is a route parameter.
@ApiQuery() describes query parameters - those after the ? sign in the URL. In /provinces?region=Gallia&page=2 the query parameters are region and page. It executes no database query, defines no GraphQL schema and creates no query builder for an ORM - the word "query" here refers solely to a part of the address.
The remaining places have decorators of their own: HTTP headers are described by @ApiHeader, and the request body by @ApiBody. Four different places, four different decorators; a mix-up produces no error, only documentation describing something that is not there.
The shape of @ApiQuery
Query parameters are often optional, and that is precisely what the documentation must record:
1@ApiQuery({
2 name: 'province',
3 required: false,
4})
5@Get('search')
6search(@Query('province') province?: string) {
7 return this.provincesService.search(province);
8}The written order is fixed: @ApiQuery({ opens the decorator, name: 'province', gives the parameter's name - the only obligatory field - required: false marks it optional, and }) closes it.
required defaults to true, so optionality must be stated explicitly. Omitting it is a small thing that costs somebody else's time: in Swagger UI the parameter appears as required, and someone builds a client that sends region on every call.
The order on an endpoint
Gathered together, the decorators run from the most general to the most particular:
1@ApiTags('Provinces')
2@Controller('provinces')
3export class ProvincesController {
4 @ApiOperation({ summary: 'Province details' })
5 @ApiResponse({ status: 200, description: 'Province found' })
6 @ApiResponse({ status: 404, description: 'No such province' })
7 @ApiParam({ name: 'id', description: 'The province identifier' })
8 @ApiBearerAuth('JWT-auth')
9 @Get(':id')
10 findOne(@Param('id') id: string) {
11 return this.provincesService.findOne(id);
12 }
13}It reads from top to bottom: @ApiTags('Name') on the controller class covers every method, then above the particular method @ApiOperation({ summary }) says what it does, next @ApiResponse({ status }) lists the possible answers, and finally the method body runs.
@ApiResponse() takes a status code and a description; it may be given several times, once for each possible response. In the coming lessons we shall replace it with shorter equivalents such as @ApiOkResponse.
@ApiBearerAuth('JWT-auth') marks the endpoint as protected - a token field appears beside it in Swagger UI, and the name given must match the addBearerAuth configuration.
Summary
A decree without a seal is a sheet of paper:
- Swagger decorators only describe - they do not change an endpoint's behaviour,
@ApiTags()groups endpoints and stands above the controller class,@ApiOperation()acceptssummaryanddescription- noturl/method, nothost/port, notcontroller/service,@ApiParam()describes route parameters in the URL path (those with a colon, like:id),@ApiQuery()describes query parameters after the?sign - it does not query a database, define GraphQL, or build ORM queries,- headers are described by
@ApiHeader, the request body by@ApiBody, - the shape of
@ApiQuery:@ApiQuery({βname: 'province',βrequired: falseβ}); withoutrequired: falsea parameter passes for mandatory, - the order:
@ApiTags('Name')on the class β@ApiOperation({ summary })above the method β@ApiResponse({ status })above the method β the method body, @ApiBearerAuth('JWT-auth')marks an endpoint as token-protected.
In the next lesson we go a level deeper - to documenting the DTOs themselves, that is, what actually sits inside those requests and responses. For now remember: the documentation describes what you wrote in the decorator, not what the code does. A divergence between them is worse than no documentation, because the first one somebody will believe.
Code for this lesson: src/province.controller.ts
1// Swagger decorators on the province controller
2import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common';
3import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
4
5// TODO: Add the @ApiTags('Provinciae') decorator
6@Controller('provinciae')
7export class ProvinceController {
8
9 // TODO: Add @ApiOperation with summary and description
10 // TODO: Add @ApiResponse for status 200
11 @Get()
12 findAll() {
13 return [
14 { name: 'Pannonia', governor: 'Lucius Verus' },
15 { name: 'Britannia', governor: 'Agricola' },
16 { name: 'Africa', governor: 'Septimius' },
17 ];
18 }
19
20 // TODO: Add @ApiOperation with summary
21 // TODO: Add @ApiParam describing the 'id' parameter
22 // TODO: Add @ApiResponse for 200 and 404
23 @Get(':id')
24 findOne(@Param('id') id: string) {
25 return { name: 'Pannonia', governor: 'Lucius Verus', legions: 3 };
26 }
27
28 // TODO: Add @ApiOperation with summary
29 // TODO: Add @ApiQuery for 'region' (optional)
30 // TODO: Add @ApiQuery for 'minLegions' (optional, type Number)
31 @Get('search')
32 search(
33 @Query('region') region?: string,
34 @Query('minLegions') minLegions?: number,
35 ) {
36 return [{ name: 'Pannonia', region: 'Europa', legions: 3 }];
37 }
38
39 // TODO: Add @ApiOperation with summary
40 // TODO: Add @ApiResponse for 201
41 @Post()
42 create(@Body() dto: any) {
43 return { id: '1', ...dto };
44 }
45}
46
47console.log("Swagger controller decorators:");
48console.log("@ApiTags('name') - groups endpoints");
49console.log("@ApiOperation({ summary, description })");
50console.log("@ApiResponse({ status, description })");
51console.log("@ApiParam({ name, description, example })");
52console.log("@ApiQuery({ name, required, description })");
53Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are the main properties accepted by the @ApiOperation() decorator?
2. When do we use the @ApiParam() decorator in a NestJS controller?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Add @ApiQuery to the search endpoint and @ApiParam to the endpoint with the :id parameter
- Vertical ordering
Order the Swagger decorators on an endpoint from the class decorator to the method body:
- Click in order
Arrange the elements of the @ApiQuery decorator in the correct order:
- Code editor
Add @ApiTags, @ApiOperation, @ApiResponse, @ApiParam, and @ApiQuery to the province controller