Contactez-nous

Documentation de l'API avec Swagger

Rendez votre API RESTful de tâches compréhensible et facile à utiliser en générant une documentation interactive avec Swagger UI, basée sur la spécification OpenAPI et intégrée à Express.

Rendre l'API utilisable : l'importance de la documentation

Notre API de gestion de tâches est maintenant fonctionnelle et testée, mais comment les autres développeurs (ou nous-mêmes dans le futur) sauront-ils comment l'utiliser ? Quels sont les endpoints exacts ? Quels paramètres acceptent-ils ? Quelle est la structure des données attendue en entrée et renvoyée en sortie ? Sans documentation claire, même la meilleure API reste difficilement exploitable.

La documentation d'API est essentielle pour :

  • Faciliter l'intégration : Permettre aux consommateurs (frontend, mobile, autres services) de comprendre rapidement comment interagir avec l'API.
  • Améliorer la collaboration : Servir de contrat clair entre les équipes backend et frontend.
  • Accélérer le développement : Réduire le temps passé à poser des questions ou à deviner le fonctionnement de l'API.
  • Maintenir la cohérence : Offrir une source unique de vérité sur les capacités de l'API.

Plutôt que d'écrire une documentation manuelle statique (difficile à maintenir à jour), nous allons utiliser la puissance de la spécification OpenAPI et des outils Swagger pour générer automatiquement une documentation interactive et toujours synchronisée avec notre code.

Intégration de Swagger UI avec `swagger-jsdoc` et `swagger-ui-express`

Pour intégrer la documentation à notre projet Express, nous allons utiliser deux bibliothèques populaires :

  • `swagger-jsdoc` : Lit des annotations spéciales (commentaires JSDoc avec la syntaxe OpenAPI) directement dans notre code source (au-dessus de nos définitions de routes) et génère la spécification OpenAPI au format JSON.
  • `swagger-ui-express` : Prend la spécification OpenAPI générée et expose une interface utilisateur web interactive (Swagger UI) sur une route spécifique de notre application Express.

Cette approche a l'avantage de garder la documentation au plus près du code qui l'implémente, facilitant ainsi sa mise à jour.

Commençons par installer ces dépendances (si ce n'est pas déjà fait dans les étapes précédentes pour la gestion des API) :

npm install swagger-ui-express swagger-jsdoc

Ensuite, configurons-les dans notre fichier serveur principal (`src/server.js`).

// src/server.js (ajouts pour Swagger)
require('dotenv').config();
const express = require('express');
const connectDB = require('./db/mongoose');
const taskRouter = require('./routes/taskRoutes');
// NOUVEAUX IMPORTS
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');

const app = express();
const port = process.env.PORT || 3000;

// Configuration swagger-jsdoc
const swaggerOptions = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'API de Gestion de Tâches',
      version: '1.0.0',
      description: 'Une API RESTful simple pour gérer des tâches, créée avec Node.js, Express et MongoDB.',
    },
    servers: [
      {
        url: `http://localhost:${port}`, // URL de base de votre API
        description: 'Serveur de développement local'
      },
      // Vous pouvez ajouter d'autres serveurs (staging, production)
    ],
    // Définition globale des composants (ex: schémas réutilisables)
    components: {
      schemas: {
        Task: {
          type: 'object',
          required: ['title'],
          properties: {
            _id: {
              type: 'string',
              description: 'ID unique généré par MongoDB',
              example: '605c72ef9f6d8e3e4c8b4567'
            },
            title: {
              type: 'string',
              description: 'Titre de la tâche',
              example: 'Faire les courses'
            },
            description: {
              type: 'string',
              description: 'Description détaillée de la tâche',
              example: 'Acheter du lait, des oeufs et du pain'
            },
            completed: {
              type: 'boolean',
              description: 'Indique si la tâche est terminée',
              default: false
            },
            createdAt: {
              type: 'string',
              format: 'date-time',
              description: 'Date de création (automatique)'
            },
            updatedAt: {
              type: 'string',
              format: 'date-time',
              description: 'Date de dernière mise à jour (automatique)'
            }
          }
        },
        NewTask: { // Schéma pour la création (sans _id, createdAt, updatedAt)
           type: 'object',
           required: ['title'],
           properties: {
             title: { type: 'string', example: 'Nouvelle tâche' },
             description: { type: 'string', example: 'Description optionnelle' },
             completed: { type: 'boolean', default: false }
           }
        },
        UpdateTask: { // Schéma pour la mise à jour (tous champs optionnels)
           type: 'object',
           properties: {
             title: { type: 'string', example: 'Titre mis à jour' },
             description: { type: 'string', example: 'Description mise à jour' },
             completed: { type: 'boolean', example: true }
           }
        },
        Error: {
           type: 'object',
           properties: {
             error: { type: 'string', example: 'Message d\'erreur descriptif' }
           }
        }
      }
    }
  },
  // Chemin vers les fichiers contenant les annotations OpenAPI (vos fichiers de routes)
  apis: ['./src/routes/*.js'], 
};

const openapiSpecification = swaggerJsdoc(swaggerOptions);

connectDB();
app.use(express.json());

// Monter la documentation Swagger UI sur /api-docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpecification));

app.use(taskRouter);
app.use((req, res, next) => { res.status(404).send({ error: 'Route non trouvée' }); });
app.use((err, req, res, next) => { /* ... */ });

if (require.main === module) {
    app.listen(port, () => {
        console.log(`Serveur démarré sur http://localhost:${port}`);
        console.log(`Documentation API disponible sur http://localhost:${port}/api-docs`);
    });
}

module.exports = app;

Notez la section `components.schemas` où nous définissons la structure de notre `Task` et des variations pour la création (`NewTask`) et la mise à jour (`UpdateTask`), ainsi qu'un schéma d'erreur générique. Cela nous permettra de référencer ces schémas dans nos annotations.

Annoter les routes avec la syntaxe OpenAPI (JSDoc)

Maintenant, ajoutons les annotations JSDoc `@openapi` au-dessus de chaque gestionnaire de route dans notre fichier `src/routes/taskRoutes.js`. Ces commentaires décrivent chaque endpoint.

Exemple pour POST /tasks :

// src/routes/taskRoutes.js (début)
const express = require('express');
const Task = require('../models/task');
const mongoose = require('mongoose'); // Assurez-vous d'importer mongoose ici
const router = express.Router();

/**
 * @openapi
 * /tasks:
 *   post:
 *     summary: Crée une nouvelle tâche
 *     tags: [Tasks]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/NewTask' # Référence au schéma défini globalement
 *     responses:
 *       201:
 *         description: Tâche créée avec succès
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Task'
 *       400:
 *         description: Données d'entrée invalides
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 *       500:
 *          description: Erreur serveur
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 */
router.post('/tasks', async (req, res) => {
  // ... (logique existante)
});

Exemple pour GET /tasks :

// src/routes/taskRoutes.js (suite)
/**
 * @openapi
 * /tasks:
 *   get:
 *     summary: Récupère la liste de toutes les tâches
 *     tags: [Tasks]
 *     responses:
 *       200:
 *         description: Une liste de tâches.
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/Task'
 *       500:
 *          description: Erreur serveur
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 */
router.get('/tasks', async (req, res) => {
  // ... (logique existante)
});

Exemple pour GET /tasks/:id :

// src/routes/taskRoutes.js (suite)
/**
 * @openapi
 * /tasks/{id}:
 *   get:
 *     summary: Récupère une tâche spécifique par son ID
 *     tags: [Tasks]
 *     parameters:
 *       - in: path
 *         name: id
 *         schema:
 *           type: string
 *           example: 605c72ef9f6d8e3e4c8b4567
 *         required: true
 *         description: ID MongoDB unique de la tâche à récupérer
 *     responses:
 *       200:
 *         description: Détails de la tâche.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Task'
 *       400:
 *          description: ID invalide fourni
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 *       404:
 *         description: Tâche non trouvée
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 *       500:
 *          description: Erreur serveur
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 */
router.get('/tasks/:id', async (req, res) => {
  // ... (logique existante)
});

Exemple pour PATCH /tasks/:id :

// src/routes/taskRoutes.js (suite)
/**
 * @openapi
 * /tasks/{id}:
 *   patch:
 *     summary: Met à jour partiellement une tâche existante
 *     tags: [Tasks]
 *     parameters:
 *       - in: path
 *         name: id
 *         schema:
 *           type: string
 *           example: 605c72ef9f6d8e3e4c8b4567
 *         required: true
 *         description: ID MongoDB unique de la tâche à mettre à jour
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/UpdateTask' # Référence au schéma de mise à jour
 *     responses:
 *       200:
 *         description: Tâche mise à jour avec succès
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Task'
 *       400:
 *         description: Données d'entrée invalides ou ID invalide
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 *       404:
 *         description: Tâche non trouvée
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 *       500:
 *          description: Erreur serveur
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 */
router.patch('/tasks/:id', async (req, res) => {
    // ... (logique existante)
});

Exemple pour DELETE /tasks/:id :

// src/routes/taskRoutes.js (suite)
/**
 * @openapi
 * /tasks/{id}:
 *   delete:
 *     summary: Supprime une tâche par son ID
 *     tags: [Tasks]
 *     parameters:
 *       - in: path
 *         name: id
 *         schema:
 *           type: string
 *           example: 605c72ef9f6d8e3e4c8b4567
 *         required: true
 *         description: ID MongoDB unique de la tâche à supprimer
 *     responses:
 *       204:
 *         description: Tâche supprimée avec succès (pas de contenu)
 *       400:
 *          description: ID invalide fourni
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 *       404:
 *         description: Tâche non trouvée
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Error'
 *       500:
 *          description: Erreur serveur
 *          content:
 *            application/json:
 *              schema:
 *                 $ref: '#/components/schemas/Error'
 */
router.delete('/tasks/:id', async (req, res) => {
  // ... (logique existante)
});

module.exports = router;

Ces annotations utilisent la syntaxe YAML au sein des commentaires JSDoc pour décrire chaque aspect de l'endpoint conformément à la spécification OpenAPI 3.0. Notez l'utilisation des `tags` pour regrouper les endpoints, des `parameters` pour les variables de chemin, de `requestBody` pour décrire les données envoyées, et de `responses` pour détailler les réponses possibles avec leurs codes de statut et leurs schémas associés (en utilisant `$ref` pour faire référence aux schémas définis globalement).

Visualiser et tester l'API avec Swagger UI

Une fois ces annotations ajoutées et la configuration dans `server.js` effectuée, relancez votre serveur Node.js (`npm run dev`).

Ouvrez maintenant votre navigateur et allez à l'URL que nous avons configurée, par exemple : `http://localhost:3000/api-docs`.

Vous devriez voir l'interface utilisateur Swagger UI s'afficher, présentant de manière claire et organisée tous les endpoints de votre API que vous venez de documenter. Vous pouvez :

  • Voir la liste des endpoints groupés par tag (`Tasks`).
  • Développer chaque endpoint pour voir sa description, ses paramètres, le format attendu du corps de la requête, et les réponses possibles avec leurs schémas.
  • Utiliser le bouton "Try it out" pour envoyer directement des requêtes à votre API locale depuis l'interface Swagger UI, remplir les paramètres ou le corps de la requête, et voir la réponse réelle du serveur.

Cette interface interactive est extrêmement utile pour tester rapidement votre API pendant le développement et constitue une documentation de référence précieuse pour tous les consommateurs de votre API. Maintenir ces annotations à jour au fur et à mesure que votre API évolue garantit que votre documentation reste toujours précise.