Cours NestJS · Module 11: Swagger et OpenAPI
Le versionnage d'API - les ères de l'Empire
Dans cette leçon6
Un changement dans une API dont dépendent plusieurs clients a une propriété fâcheuse : on ne peut pas le retirer des applications d'autrui. Tu supprimes un champ que personne - crois-tu - n'utilise, et l'après-midi même tu apprends qu'un intégrateur à l'autre bout du pays s'en servait.
Rome réglait cela par des ères. Le droit de la République ne disparaissait pas à l'avènement de l'Empire - il continuait de régir les affaires ouvertes auparavant, tandis que les règles nouvelles couraient à côté. Le versionnage d'API est la même chose : l'ancienne version vit tant que quelqu'un s'en sert, et les changements vont dans la nouvelle.
Trois stratégies
NestJS connaît trois façons d'indiquer une version :
- URI Versioning - la version dans le chemin, comme
/v1/legiones. La plus employée, car elle se voit dans les journaux, dans le navigateur et dans les favoris. - Header Versioning - la version dans un en-tête HTTP, par exemple
X-Version: 1. L'adresse reste nette, mais la version ne se voit pas sans inspecter la requête. - Media Type Versioning - la version dans l'en-tête
Content-Type. La plus fidèle à l'esprit d'HTTP et la plus rare en pratique.
Avec l'URI Versioning en version un, l'adresse se présente ainsi : /v1/legiones. Non /legiones?version=1 - ce serait un paramètre de requête, non une version. Non /legiones/v1 - là, v1 aurait l'air d'un identifiant de ressource. Ni /legiones avec un en-tête X-Version - c'est déjà une autre stratégie.
L'activation du versionnage
Le versionnage s'active une fois, dans main.ts :
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.enableVersioning({
5 type: VersioningType.URI,
6 defaultVersion: '1',
7 });
8
9 await app.listen(3000);
10}L'écriture comporte quatre parties dans un ordre fixe : app.enableVersioning({ ouvre la configuration, type: VersioningType.URI, choisit la stratégie, defaultVersion: '1' fixe la version des contrôleurs qui n'en déclarent pas, et }); referme.
defaultVersion mérite d'être posé d'emblée. Sans lui, tout contrôleur dépourvu de version explicite cesse de répondre - et lorsqu'on active le versionnage dans un projet existant, cela veut dire d'ordinaire tous les contrôleurs à la fois.
Un contrôleur versionné
Un contrôleur déclare sa version dans le décorateur même qui indique son chemin :
1@Controller({
2 path: 'legiones', version: '1' })
3export class LegionV1Controller {}L'ordre d'écriture est fixe : @Controller({ ouvre l'objet de configuration, path: 'legiones', version: '1' }) fournit le chemin et la version, et export class LegionV1Controller {} déclare la classe elle-même.
Remarque que @Controller('legiones') avec une chaîne devient ici @Controller({ path, version }) avec un objet - la même fonction, dans la variante qui accepte davantage d'options. Des méthodes isolées se versionnent à part par le décorateur @Version('2'), quand une seule d'entre elles a changé.
Le nom de la classe - LegionV1Controller - n'importe pas au routage ; la version se décide par le seul champ version. Il vaut néanmoins la peine de le nommer d'après la version : dans un projet à deux ères, deux classes LegionController dans des fichiers différents deviennent vite une source d'erreurs.
Une documentation distincte par version
Un document Swagger unique où les versions se mêlent est moins utile que deux documents séparés. Le processus compte quatre étapes, dans cet ordre :
app.enableVersioning()- l'application doit d'abord savoir distinguer les versions.- Un
DocumentBuilderpar version - une configuration distincte, avec son titre et son numéro. SwaggerModule.createDocumentavec l'optioninclude- la construction d'un document à partir des seuls modules choisis.SwaggerModule.setuppar version - la publication de chaque document à sa propre adresse.
1const configV1 = new DocumentBuilder()
2 .setTitle('Legion API v1')
3 .setDescription('La premiere ere de l Empire')
4 .setVersion('1.0')
5 .setContact('Chancellerie', 'https://imperium.rome', 'chancellerie@imperium.rome')
6 .setLicense('MIT', 'https://opensource.org/licenses/MIT')
7 .addServer('https://api.imperium.rome')
8 .addTag('Legions')
9 .addBearerAuth(undefined, 'JWT-auth')
10 .build();
11
12const documentV1 = SwaggerModule.createDocument(app, configV1, {
13 include: [LegionV1Module],
14});
15
16SwaggerModule.setup('api/v1', app, documentV1);Un DocumentBuilder se compose d'une chaîne de méthodes, chacune ajoutant un élément de la description : setTitle, setDescription et setVersion sont la base, setContact indique à qui écrire, setLicense donne la licence, addServer l'adresse à laquelle l'API tourne vraiment, addTag déclare un groupe d'endpoints, et addBearerAuth active le champ de token.
L'option include n'inclut que les modules sélectionnés dans la documentation. Elle n'ajoute pas de fichiers CSS à Swagger UI, n'importe pas de spécifications OpenAPI externes et n'active pas de décorateurs supplémentaires. C'est elle qui fait que le document v1 ne contient que les endpoints de la première ère - sans elle, les deux documents seraient identiques et montreraient toute l'API.
Enfin, SwaggerModule.setup('api/v1', ...) publie le document à l'adresse /api/v1. La seconde version se bâtit de même : un configV2 avec son titre et son numéro, include: [LegionV2Module], et setup('api/v2', ...) pour finir.
La dépréciation des endpoints
La version deux n'abolit pas la version un du jour au lendemain. Les endpoints appelés à disparaître sont d'abord marqués comme dépréciés :
1@ApiOperation({
2 summary: 'Liste des legions (ancienne version)',
3 deprecated: true,
4})
5@Get()
6findAllLegacy() {
7 return this.legionService.findAll();
8}Tu marques un endpoint comme déprécié par la propriété deprecated: true dans @ApiOperation. Non removed, non obsolete, non disabled - ces trois noms sonnent bien, mais ils n'existent pas dans OpenAPI ; la spécification ne connaît que deprecated.
Dans Swagger UI, un tel endpoint est barré et assorti d'un avertissement, mais il fonctionne toujours. C'est tout l'objet : le marquage est une annonce, non une extinction. Il laisse aux intégrateurs le temps de migrer avant que l'endpoint ne disparaisse pour de bon - et sans cette période de transition, le versionnage n'a pas de sens, puisque le changement casse de toute façon les applications d'autrui.
Récapitulatif
Le droit de la République vaut tant que demeurent des affaires ouvertes sous la République :
- trois stratégies : URI (version dans le chemin), Header (dans un en-tête, p. ex.
X-Version), Media Type (dansContent-Type), - avec l'URI Versioning, l'adresse est
/v1/legiones- non/legiones?version=1, non/legiones/v1, - activation dans
main.ts:app.enableVersioning({→type: VersioningType.URI,→defaultVersion: '1'→});, defaultVersionprotège les contrôleurs qui ne déclarent pas de version,- contrôleur versionné :
@Controller({→path: 'legiones', version: '1' })→export class LegionV1Controller {}, - documentation distincte en quatre étapes :
app.enableVersioning()→ unDocumentBuilderpar version →createDocumentavecinclude→SwaggerModule.setuppar version, DocumentBuilder:setTitle,setDescription,setVersion,setContact,setLicense,addServer,addTag,addBearerAuth,- l'option
includen'inclut que les modules sélectionnés - elle n'ajoute pas de CSS, n'importe pas de spécifications externes, n'active pas de décorateurs, deprecated: truedans@ApiOperationmarque un endpoint comme déprécié - nonremoved, nonobsolete, nondisabled; l'endpoint fonctionne encore.
Dans la prochaine leçon, nous rassemblerons tout le module en un projet - la documentation complète de l'Imperium API. En attendant, retiens ceci : le versionnage n'existe pas pour te permettre de changer les choses. Il existe pour qu'un changement ne gâche la journée de personne.
Code de cette leçon : src/versioning.ts
1// Versionnage de l'API avec la documentation Swagger
2import { VersioningType } from '@nestjs/common';
3import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4
5// Configuration du versionnage dans main.ts
6async function bootstrap() {
7 // const app = await NestFactory.create(AppModule);
8
9 // TODO: Active l'URI versioning avec defaultVersion '1'
10 // app.enableVersioning({
11 // type: VersioningType.URI,
12 // defaultVersion: '1',
13 // });
14
15 // TODO: Crée un DocumentBuilder pour la version 1
16 const configV1 = new DocumentBuilder()
17 .setTitle('Imperium API v1')
18 .setDescription('Ère de la République')
19 .setVersion('1.0')
20 .build();
21
22 // TODO: Crée un DocumentBuilder pour la version 2
23 // const configV2 = new DocumentBuilder()
24 // .setTitle('Imperium API v2')
25 // .setDescription('Ère de l'Empire')
26 // .setVersion('2.0')
27 // .build();
28
29 // TODO: Crée des documents Swagger distincts pour chaque version
30 // const docV1 = SwaggerModule.createDocument(app, configV1);
31 // SwaggerModule.setup('api/v1/docs', app, docV1);
32
33 // const docV2 = SwaggerModule.createDocument(app, configV2);
34 // SwaggerModule.setup('api/v2/docs', app, docV2);
35
36 console.log("Swagger v1: http://localhost:3000/api/v1/docs");
37 console.log("Swagger v2: http://localhost:3000/api/v2/docs");
38}
39
40bootstrap();
41
42console.log("=== Stratégies de versionnage ===");
43console.log("URI: /v1/legiones, /v2/legiones");
44console.log("Header: X-API-Version: 1");
45console.log("Media Type: Accept: application/json;v=1");
46console.log("");
47console.log("Chaque version peut avoir son propre document Swagger");
48console.log("deprecated: true marque un endpoint retiré");
49Tu 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. À quoi ressemble l'URL de l'endpoint avec l'URI Versioning en version 1 ?
2. Comment marquer un endpoint comme déprécié dans @ApiOperation ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Classe les étapes de création d'une API versionnée avec des documents Swagger distincts :
- Éditeur de code
Crée un DocumentBuilder pour la version 1 et la version 2 de l'API avec les réglages appropriés
- Tri vertical
Ordonne les éléments d'une déclaration de contrôleur versionné dans le bon ordre :
- Clique dans l'ordre
Ordonne les éléments pour activer l'URI Versioning dans main.ts :
- Éditeur de code
Crée une configuration avec title, description, version, contact, license, servers, tags et bearerAuth