Cours NestJS · Module 11: Swagger et OpenAPI
Réponses avancées - les édits impériaux
Dans cette leçon7
Un édit impérial ne s'arrêtait pas à l'ordre. Il énumérait toujours les conséquences : ce qui advient une fois l'ordre exécuté, ce qu'il en est si le pétitionnaire n'a pas le droit de demander, et ce qu'il en est si l'affaire ne figure pas du tout dans les annales. Seul un tel document pouvait s'appliquer sans renvoyer la question à la chancellerie.
Ton API fonctionne de même. Dans la leçon précédente, nous documentions une réponse par le @ApiResponse général, en indiquant le code de statut à la main. Mais NestJS possède un décorateur distinct pour chaque code - et cette leçon leur est consacrée.
Le décorateur décrit, il ne définit pas
Commençons par ce qui prête souvent à confusion. Ces décorateurs ne changent pas le comportement de l'application. Ils vont dans la documentation, et nulle part ailleurs :
1@ApiResponse({ status: 200, description: 'Liste des légions' })
2@Get()
3findAll() {}
4
5@ApiOkResponse({ description: 'Liste des légions' })
6@Get()
7findAll() {}Les deux écritures produisent exactement la même chose dans Swagger UI. La seconde est plus brève et se trompe moins facilement, car tu n'y fournis pas le code de statut : il siège dans le nom du décorateur.
Ce que le client reçoit réellement se décide ailleurs : @Post renvoie 201 par défaut, les autres méthodes 200, et @HttpCode(204) l'emporte explicitement. Ajouter @ApiCreatedResponse au-dessus d'une méthode @Get ne lui fera pas renvoyer 201 - cela fera seulement mentir la documentation.
Le code loge dans le nom du décorateur
La règle est simple : le nom du décorateur est le nom officiel du code HTTP. Tout le jeu se reconstitue donc de mémoire :
1import {
2 ApiOkResponse, // 200 OK
3 ApiCreatedResponse, // 201 Created
4 ApiBadRequestResponse, // 400 Bad Request
5 ApiUnauthorizedResponse, // 401 Unauthorized
6 ApiForbiddenResponse, // 403 Forbidden
7 ApiNotFoundResponse, // 404 Not Found
8 ApiInternalServerErrorResponse, // 500 Internal Server Error
9} from '@nestjs/swagger';@ApiCreatedResponse() correspond au code 201 (Created), et @ApiNotFoundResponse() représente le code 404 Not Found - non 400, non 401 et non 403, car chacun d'eux possède son propre décorateur dans la liste ci-dessus.
La règle vaut aussi dans l'autre sens et permet d'écarter les noms qui n'existent pas. @ApiNewResponse() et @ApiSuccessResponse() n'existent pas - ils sonnent bien, mais HTTP ne comporte aucun code nommé « New » ni « Success ». Si tu ne sais pas nommer le code auquel un décorateur renvoie, c'est le signe que ce décorateur n'existe pas.
Trois classes de codes
Les codes se rangent en classes, et la classe dit à qui revient la faute :
- 2xx - succès.
200 OKsignifie « c'est fait » ;201 Createdsignifie « c'est fait, et une ressource nouvelle existe ». - 4xx - faute du client. La requête était mal formée ou non permise ; la répéter telle quelle n'aboutira à rien.
- 5xx - faute du serveur.
500 Internal Server Errorsignifie que l'application s'est effondrée. La requête pouvait être parfaitement correcte.
Classés du succès à l'erreur serveur, ils donnent : 200 OK → 201 Created → 401 Unauthorized → 500 Internal Server Error.
Au sein des 4xx, une paire mérite d'être retenue. 401 Unauthorized signifie « je ne sais pas qui tu es » - le token manque ou n'est pas valide. 403 Forbidden signifie « je sais qui tu es, et tu n'as pas le droit » - le token est bon, mais le rôle ne suffit pas. Le premier se corrige en s'authentifiant ; le second ne se corrige pas du tout.
La forme du décorateur
Le décorateur reçoit un objet, et l'ordre de ses champs est conventionnel, quoique toujours le même en pratique - la description d'abord, le type ensuite :
1@ApiOkResponse({
2 description: 'Liste des légions',
3 type: LegionResponseDto,
4})
5@Get()
6findAll(): LegionResponseDto[] {
7 return this.legionService.findAll();
8}Cela se lit en quatre temps : @ApiOkResponse({ ouvre le décorateur, description: 'Liste des légions', explique à un humain ce qu'il recevra, type: LegionResponseDto désigne la classe DTO, et }) referme.
description devient la description de la réponse dans Swagger UI. type fait davantage : Swagger va chercher les décorateurs @ApiProperty de cette classe - ceux de la leçon précédente - et en construit un schéma de réponse complet, valeurs d'exemple comprises. Sans type, le lecteur apprend qu'il recevra un 200, mais non ce qu'il contient.
Une réponse qui est un tableau
Quand un endpoint renvoie une liste, tu disposes de deux écritures équivalentes, et toutes deux fonctionnent correctement :
1@ApiOkResponse({ type: [LegionResponseDto] })
2@Get()
3findAll() {}
4
5@ApiOkResponse({ type: LegionResponseDto, isArray: true })
6@Get()
7findAllAgain() {}La première - la classe entre crochets - est plus brève. La seconde, avec un isArray: true explicite, se lit parfois mieux lorsque d'autres options l'accompagnent. Le choix relève du style ; Swagger engendre un schéma identique.
Ce qui ne fonctionnera pas, c'est type: Array<LegionResponseDto>. La raison va plus loin que la syntaxe : les types génériques disparaissent à la compilation du TypeScript. Un décorateur reçoit une valeur existant à l'exécution, et après compilation il ne reste d'Array<LegionResponseDto> que Array - sans trace de ce dont il est le tableau. Les crochets et isArray fonctionnent précisément parce qu'ils transmettent la classe LegionResponseDto comme une valeur ordinaire.
Le jeu complet sur un endpoint
En pratique, un endpoint documente toutes les réponses qu'un client peut rencontrer :
1@ApiOperation({ summary: 'Promouvoir un légionnaire' })
2@ApiOkResponse({
3 description: 'Le legionnaire a ete promu',
4 type: LegionaryResponseDto,
5})
6@ApiBadRequestResponse({ description: 'Rang cible invalide' })
7@ApiUnauthorizedResponse({ description: 'Aucun token JWT valide' })
8@ApiNotFoundResponse({ description: 'Legionnaire introuvable' })
9@ApiBearerAuth('JWT-auth')
10@UseGuards(JwtAuthGuard)
11@Patch(':id/promote')
12promote(@Param('id') id: string, @Body() dto: PromoteLegionaryDto) {
13 return this.legionService.promote(id, dto);
14}Remarque la paire @ApiBearerAuth('JWT-auth') et @ApiUnauthorizedResponse. Le premier marque l'endpoint comme protégé et ajoute un champ de token dans Swagger UI - le nom 'JWT-auth' doit correspondre à celui donné lors de la configuration d'addBearerAuth. Le second décrit ce qui advient sans token. Un endpoint protégé sans @ApiUnauthorizedResponse promet au lecteur qu'un 401 n'arrivera jamais - et il arrivera dès la première tentative.
Récapitulatif
Un édit énumère ses conséquences :
- les décorateurs de réponse ne font que documenter ; le code de statut est fixé par
@Post,@Getou@HttpCode, - le nom du décorateur est le nom du code :
@ApiOkResponse(200),@ApiCreatedResponse(201),@ApiBadRequestResponse(400),@ApiUnauthorizedResponse(401),@ApiForbiddenResponse(403),@ApiNotFoundResponse(404),@ApiInternalServerErrorResponse(500), @ApiNewResponseet@ApiSuccessResponsen'existent pas - aucun code HTTP ne porte ces noms,- classes de codes : 2xx succès, 4xx faute du client, 5xx faute du serveur ; du succès à l'erreur serveur : 200 OK → 201 Created → 401 Unauthorized → 500 Internal Server Error,
- 401 signifie « je ne sais pas qui tu es », 403 « je le sais, et tu n'as pas le droit »,
- la forme du décorateur :
@ApiOkResponse({→description: 'Liste des légions',→type: LegionResponseDto→}), typelie la réponse à un DTO, et Swagger en construit le schéma à partir de ses@ApiProperty,- un tableau s'écrit de deux façons équivalentes :
type: [LegionDto]outype: LegionDto, isArray: true- les deux fonctionnent correctement, type: Array<LegionDto>ne fonctionnera pas, car les types génériques disparaissent à la compilation,- un endpoint protégé se marque par la paire
@ApiBearerAuth('JWT-auth')et@ApiUnauthorizedResponse.
Dans la prochaine leçon, nous nous occuperons de l'apparence du Forum des Annales lui-même - la configuration de Swagger UI. En attendant, retiens ceci : la documentation des réponses n'est pas un ornement. C'est le seul endroit où un client apprend à quoi se préparer, avant que son code ne le découvre à ses dépens.
Code de cette leçon : src/tributum.controller.ts
1// Décorateurs de réponse avancés
2import { Controller, Get, Post, Param, Body, Delete, UseGuards } from '@nestjs/common';
3import {
4 ApiTags, ApiOperation, ApiBearerAuth,
5 ApiOkResponse, ApiCreatedResponse,
6 ApiNotFoundResponse, ApiBadRequestResponse,
7 ApiUnauthorizedResponse,
8} from '@nestjs/swagger';
9
10@ApiTags('Tributum')
11@Controller('tributum')
12export class TributumController {
13
14 // TODO: Ajoute @ApiOperation avec summary
15 // TODO: Ajoute @ApiOkResponse avec description
16 @Get()
17 findAll() {
18 return [
19 { id: '1', province: 'Pannonia', amount: 50000, status: 'collected' },
20 { id: '2', province: 'Britannia', amount: 30000, status: 'pending' },
21 ];
22 }
23
24 // TODO: Ajoute @ApiOperation avec summary
25 // TODO: Ajoute @ApiCreatedResponse avec description
26 // TODO: Ajoute @ApiBadRequestResponse avec description
27 @Post()
28 create(@Body() dto: any) {
29 return { id: '3', ...dto, status: 'pending' };
30 }
31
32 // TODO: Ajoute @ApiOperation avec summary
33 // TODO: Ajoute @ApiOkResponse avec description
34 // TODO: Ajoute @ApiNotFoundResponse avec description
35 @Get(':id')
36 findOne(@Param('id') id: string) {
37 return { id, province: 'Pannonia', amount: 50000 };
38 }
39
40 // TODO: Ajoute @ApiBearerAuth('JWT-auth')
41 // TODO: Ajoute @ApiOperation avec summary
42 // TODO: Ajoute @ApiOkResponse avec description
43 // TODO: Ajoute @ApiNotFoundResponse avec description
44 // TODO: Ajoute @ApiUnauthorizedResponse avec description
45 @Delete(':id')
46 remove(@Param('id') id: string) {
47 return { message: 'Tribut annulé', id };
48 }
49}
50
51console.log("Décorateurs de réponse spécialisés :");
52console.log("@ApiOkResponse - 200 OK");
53console.log("@ApiCreatedResponse - 201 Created");
54console.log("@ApiNotFoundResponse - 404 Not Found");
55console.log("@ApiBadRequestResponse - 400 Bad Request");
56console.log("@ApiUnauthorizedResponse - 401 Unauthorized");
57Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel décorateur Swagger correspond au code de statut HTTP 201 (Created) ?
2. Quel code de statut HTTP le décorateur @ApiNotFoundResponse() représente-t-il ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Ajoute @ApiOkResponse, @ApiCreatedResponse, @ApiNotFoundResponse et @ApiBadRequestResponse aux endpoints
- Tri vertical
Classe les codes de statut HTTP, du succès à l'erreur serveur :
- Clique dans l'ordre
Ordonne les éléments du décorateur @ApiOkResponse avec un type DTO :
- Éditeur de code
Marque les endpoints comme protégés par JWT à l'aide de @ApiBearerAuth('JWT-auth') et @ApiUnauthorizedResponse