Une API sans documentation, c'est une boîte noire : les intégrateurs devinent, le front tâtonne, et vous répondez trois fois par semaine à la même question sur Slack. La bonne nouvelle avec NestJS, c'est que la documentation peut être générée automatiquement à partir de votre code, en restant toujours synchronisée avec la réalité.
Dans cet article, je montre comment mettre en place @nestjs/swagger de A à Z : la config de base, comment décrire proprement vos DTOs, gérer l'authentification JWT dans l'interface Swagger, organiser les endpoints par tags, et éviter les pièges classiques qui produisent une doc à moitié fausse.
Pourquoi Swagger plutôt qu'un fichier Markdown
La tentation quand on démarre un projet, c'est de documenter l'API dans un README ou un Notion. Ça marche une semaine. Puis un endpoint change de signature, un champ devient optionnel, un code d'erreur apparaît — et la doc écrite à la main diverge silencieusement du code. Personne ne s'en aperçoit avant qu'un intégrateur perde une demi-journée sur un champ qui n'existe plus.
@nestjs/swagger résout ce problème à la racine : la documentation est dérivée du code. Vos contrôleurs, vos DTOs et leurs décorateurs deviennent la source de vérité. Vous obtenez une interface Swagger UI interactive (où l'on peut tester les endpoints directement), et un document OpenAPI standard exploitable par des générateurs de clients TypeScript, des outils de tests de contrat, ou Postman.
L'autre bénéfice, moins évident : documenter force à clarifier son API. Quand vous devez expliquer ce que renvoie un endpoint, vous repérez vite les incohérences de nommage et les réponses mal typées.
Installation et configuration de base
On commence par installer le package :
npm install @nestjs/swagger
La configuration se fait dans le main.ts, au moment du bootstrap de l'application. Le principe : on construit un document à partir d'une config, puis on l'expose sur une route.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('API Boutique')
.setDescription("Documentation de l'API e-commerce")
.setVersion('1.0')
.addTag('produits', 'Gestion du catalogue')
.addTag('commandes', 'Cycle de vie des commandes')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // conserve le token entre les rechargements
},
});
await app.listen(3000);
}
bootstrap();
Lancez votre serveur, ouvrez http://localhost:3000/docs, et vous avez déjà une interface Swagger fonctionnelle. Le persistAuthorization: true est un petit confort qui évite de re-saisir son token JWT à chaque rafraîchissement de la page.
Activer le plugin CLI pour éviter la répétition
Par défaut, Swagger n'a aucune idée du type de vos propriétés : TypeScript est effacé à la compilation, et les métadonnées ne suffisent pas toujours. Résultat, il faut annoter chaque champ avec @ApiProperty(). Le plugin CLI de NestJS automatise une grande partie de ce travail en inférant les types depuis vos DTOs.
Activez-le dans nest-cli.json :
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger"]
}
}
Avec ce plugin, un champ typé price: number sera automatiquement documenté comme un nombre, un name?: string sera marqué optionnel, et vos commentaires JSDoc deviendront des descriptions. Vous ne gardez @ApiProperty() que pour les cas où vous voulez enrichir : exemples, contraintes, formats particuliers.
Décrire proprement ses DTOs
Le cœur d'une bonne documentation, ce sont les DTOs. C'est eux qui décrivent la forme des données en entrée et en sortie. Prenons un DTO de création de produit, combiné avec class-validator — que j'utilise systématiquement pour la validation :
// src/produits/dto/create-produit.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsOptional, Min, MaxLength } from 'class-validator';
export class CreateProduitDto {
@ApiProperty({
description: 'Nom commercial du produit',
example: 'Casque audio sans fil',
maxLength: 120,
})
@IsString()
@MaxLength(120)
name: string;
@ApiProperty({
description: 'Prix TTC en euros',
example: 89.9,
minimum: 0,
})
@IsNumber()
@Min(0)
price: number;
@ApiProperty({
description: 'Description longue affichée sur la fiche produit',
required: false,
example: 'Réduction de bruit active, autonomie 30h.',
})
@IsOptional()
@IsString()
description?: string;
}
L'intérêt est double. Les décorateurs @ApiProperty alimentent Swagger, et les décorateurs class-validator valident réellement les requêtes entrantes. Vous décrivez une contrainte (maxLength: 120) et vous l'appliquez au même endroit — ce qui garantit que la doc et le comportement réel coïncident. Si vous voulez creuser la validation côté serveur, j'en parle en détail dans mon article sur Zod et dans celui sur les pipes et interceptors NestJS.
Le piège des DTOs de réponse
Une erreur fréquente : documenter uniquement les entrées et laisser Swagger deviner les sorties. Or vos entités Mongoose ou vos objets internes contiennent souvent des champs que vous ne voulez pas exposer (__v, hash de mot de passe, champs internes). Créez des DTOs de réponse explicites et déclarez-les avec @ApiResponse :
// src/produits/dto/produit-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class ProduitResponseDto {
@ApiProperty({ example: '65f1a2b3c4d5e6f7a8b9c0d1' })
id: string;
@ApiProperty({ example: 'Casque audio sans fil' })
name: string;
@ApiProperty({ example: 89.9 })
price: number;
@ApiProperty({ example: '2026-07-24T10:30:00.000Z' })
createdAt: Date;
}
Et dans le contrôleur :
// src/produits/produits.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { ProduitsService } from './produits.service';
import { CreateProduitDto } from './dto/create-produit.dto';
import { ProduitResponseDto } from './dto/produit-response.dto';
@ApiTags('produits')
@Controller('produits')
export class ProduitsController {
constructor(private readonly produitsService: ProduitsService) {}
@Post()
@ApiOperation({ summary: 'Créer un nouveau produit' })
@ApiResponse({
status: 201,
description: 'Produit créé avec succès',
type: ProduitResponseDto,
})
@ApiResponse({ status: 400, description: 'Données invalides' })
create(@Body() dto: CreateProduitDto): Promise<ProduitResponseDto> {
return this.produitsService.create(dto);
}
@Get(':id')
@ApiOperation({ summary: 'Récupérer un produit par son identifiant' })
@ApiResponse({ status: 200, type: ProduitResponseDto })
@ApiResponse({ status: 404, description: 'Produit introuvable' })
findOne(@Param('id') id: string): Promise<ProduitResponseDto> {
return this.produitsService.findOne(id);
}
}
Le @ApiTags('produits') regroupe tous ces endpoints sous une même section dans Swagger UI. @ApiOperation donne un résumé lisible, et les multiples @ApiResponse documentent les codes de statut possibles — y compris les erreurs, que trop de docs oublient.
Gérer l'authentification JWT dans Swagger
Dès que votre API est protégée, il faut pouvoir tester les endpoints authentifiés directement depuis Swagger UI. On déclare le schéma de sécurité dans le DocumentBuilder, puis on marque les routes concernées.
Dans le main.ts, on ajoute la config bearer :
const config = new DocumentBuilder()
.setTitle('API Boutique')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Collez votre token JWT ici',
},
'access-token', // nom de référence du schéma
)
.build();
Ensuite, sur les contrôleurs ou routes protégées, on référence ce schéma :
import { ApiBearerAuth } from '@nestjs/swagger';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@ApiTags('commandes')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('commandes')
export class CommandesController {
// ...
}
Un bouton « Authorize » apparaît alors dans Swagger UI. On y colle son token une fois, et toutes les requêtes de test envoient automatiquement l'en-tête Authorization: Bearer .... Si la mise en place des guards JWT et des rôles vous intéresse, j'ai écrit un guide dédié sur les Guards NestJS avec JWT et rôles.
Documenter les cas avancés
Les tableaux et types génériques
Swagger a besoin d'aide pour les tableaux d'objets, car TypeScript n'expose pas le type des éléments à l'exécution. On utilise l'option type avec une référence explicite :
@Get()
@ApiOperation({ summary: 'Lister tous les produits' })
@ApiResponse({ status: 200, type: [ProduitResponseDto] })
findAll(): Promise<ProduitResponseDto[]> {
return this.produitsService.findAll();
}
Les crochets [ProduitResponseDto] indiquent un tableau. Pour des réponses paginées génériques (une enveloppe { data, total, page }), on crée un DTO générique et on utilise getSchemaPath avec @ApiExtraModels — un peu plus verbeux, mais ça évite de dupliquer une enveloppe de pagination dans chaque endpoint.
Les paramètres de requête
Pour les filtres et la pagination passés en query string, @ApiQuery documente chaque paramètre :
import { ApiQuery } from '@nestjs/swagger';
@Get()
@ApiQuery({ name: 'page', required: false, example: 1 })
@ApiQuery({ name: 'limit', required: false, example: 20 })
@ApiQuery({ name: 'search', required: false, description: 'Recherche par nom' })
findAll(/* ... */) {
// ...
}
Là encore, si vous utilisez un DTO de query annoté avec @ApiProperty, le plugin CLI génère ces @ApiQuery automatiquement — préférez toujours cette approche, plus maintenable.
Trois erreurs à éviter
Exposer Swagger en production sans réflexion. Une doc publique révèle toute la surface de votre API. Sur un projet interne ou une API publique documentée, c'est voulu. Sinon, conditionnez le SwaggerModule.setup() à l'environnement, ou protégez la route /docs par une authentification basique.
if (process.env.NODE_ENV !== 'production') {
SwaggerModule.setup('docs', app, document);
}
Oublier de documenter les erreurs. Un endpoint qui ne liste que sa réponse 200 est à moitié documenté. Les intégrateurs ont besoin de connaître les 400, 401, 404 et 409 pour gérer les cas d'échec. Un décorateur custom regroupant les réponses d'erreur communes évite la répétition.
Laisser diverger les exemples. Les example dans @ApiProperty sont précieux, mais ils ne sont pas validés contre vos contraintes. Un exemple price: -5 sur un champ @Min(0) passera sans broncher et induira en erreur. Relisez vos exemples comme du vrai code.
Conclusion
Mettre en place @nestjs/swagger prend une heure sur un projet existant, et transforme votre API en un contrat vivant, toujours à jour avec le code. Le trio gagnant : le plugin CLI pour éviter la répétition, des DTOs de réponse explicites pour ne rien exposer par accident, et une documentation complète des cas d'erreur et de l'authentification. Le document OpenAPI généré devient ensuite une brique réutilisable — génération de clients typés, tests de contrat, import dans Postman.
L'investissement se rentabilise dès la première intégration : moins de questions, moins d'allers-retours, et une API que les autres développeurs comprennent sans vous solliciter.
Vous avez un projet d'API ou de site web sur mesure ? N'hésitez pas à me contacter pour en discuter — vous pouvez aussi m'écrire directement à contact@alexis-mouchon.fr.