Skip to content

Créer des Routes dans le Serveur Express

Rémi Bernard edited this page Jun 16, 2025 · 1 revision

Ce guide explique comment créer et organiser les routes API dans notre backend Express.js, en suivant les patterns et conventions établis utilisés dans tout le projet.

📝 Note importante : Dans ce guide, les commentaires dans le code sont en français pour faciliter l'apprentissage et les explications. Dans l'application réelle, tous les commentaires doivent être écrits en anglais pour maintenir la cohérence.

📋 Structure d'un Fichier de Routes

Chaque fichier de routes suit la même structure de base :

import { Router, Request, Response } from 'express';
import { query, param, body } from 'express-validator';
import Model from '../models/model-name';
import { authMiddleware } from '../middleware/auth-middleware';
import { adminMiddleware } from '../middleware/roles-middleware';
import { validateRequest } from '../middleware/validate-request';
import { AppError } from '../utils/app-error';
import { asyncHandler } from '../utils/async-handler';

const router = Router();

// Appliquer le middleware à toutes les routes de ce fichier
router.use(authMiddleware);

// Définir vos routes ici
// ... définitions des routes

export default router;

🔧 Composants Essentiels

Imports Requis

Composants Express

import { Router, Request, Response } from 'express';

Outils de Validation

import { query, param, body } from 'express-validator';

Middlewares

import { authMiddleware } from '../middleware/auth-middleware';
import { adminMiddleware, teacherMiddleware } from '../middleware/roles-middleware';
import { validateRequest } from '../middleware/validate-request';

Utilitaires

import { AppError } from '../utils/app-error';
import { asyncHandler } from '../utils/async-handler';

Modèles de Base de Données

import ModelName from '../models/model-name';

🔄 Concept des Middlewares

Qu'est-ce qu'un Middleware ?

Un middleware est une fonction qui s'exécute entre la réception d'une requête HTTP et l'envoi de la réponse. C'est le concept central d'Express.js qui permet de traiter les requêtes de manière modulaire et réutilisable.

Anatomie d'un middleware :

const monMiddleware = (req: Request, res: Response, next: NextFunction) => {
    // 1. Traitement avant la route principale
    console.log('Requête reçue:', req.method, req.url);
    
    // 2. Modification possible des objets req/res
    req.user = { id: '123', name: 'Jean' };
    
    // 3. Appel de next() pour passer au middleware suivant
    next(); // Sans cet appel, la requête reste bloquée !
};

Comment ça fonctionne ?

Les middlewares forment une chaîne de traitement (pipeline) :

// Middleware global (s'applique à toutes les routes)
app.use(cors());
app.use(express.json());

// Middleware de route spécifique
router.get('/users', 
    authMiddleware,        // 1er: Vérifier l'authentification
    adminMiddleware,       // 2ème: Vérifier les droits admin
    validateRequest,       // 3ème: Valider les données
    asyncHandler(async (req, res) => {  // 4ème: Logique métier
        const users = await User.find();
        res.json({ users });
    })
);

Flux d'exécution :

Requête HTTP → cors() → express.json() → authMiddleware → adminMiddleware → validateRequest → Route Handler → Réponse HTTP

Types de Middlewares dans notre application

1. Middlewares de sécurité

// Authentification - vérifie le token JWT
router.use(authMiddleware);

// Autorisation - vérifie les rôles utilisateur
router.post('/', adminMiddleware, createItem);

2. Middlewares de validation

// Validation des données d'entrée
const validation = [
    body('email').isEmail(),
    body('password').isLength({ min: 6 })
];

router.post('/register', validation, validateRequest, register);

3. Middlewares utilitaires

// Gestion des erreurs asynchrones
router.get('/', asyncHandler(async (req, res) => {
    // Les erreurs sont automatiquement capturées
}));

Middleware personnalisé exemple :

const loggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
    const startTime = Date.now();
    
    // Continuer vers le middleware suivant
    next();
    
    // Code exécuté après la réponse (grâce aux events)
    res.on('finish', () => {
        const duration = Date.now() - startTime;
        console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`);
    });
};

Explication : Les middlewares permettent de séparer les préoccupations (authentification, validation, logging) de la logique métier, rendant le code plus maintenable et réutilisable.

🔄 Programmation Asynchrone

Introduction à async/await

La programmation asynchrone est essentielle dans Node.js pour gérer les opérations qui prennent du temps (base de données, API externes, fichiers). Voici les concepts clés :

Syntaxe async/await

// Fonction asynchrone - doit être marquée avec 'async'
const getUserData = async (userId: string) => {
    try {
        // 'await' attend que la promesse soit résolue
        const user = await User.findById(userId);
        const posts = await Post.find({ userId });
        
        return { user, posts };
    } catch (error) {
        // Gestion d'erreur automatique des promesses rejetées
        throw new AppError('Erreur lors de la récupération des données', 500);
    }
};

Explication du code :

  • async marque une fonction comme asynchrone et permet l'utilisation d'await
  • await met en pause l'exécution jusqu'à ce que la promesse soit résolue
  • Les erreurs sont automatiquement converties en exceptions catchables
  • Le code ressemble à du code synchrone mais reste non-bloquant

Parallèle avec les Coroutines Kotlin

Si vous connaissez Kotlin, les concepts sont très similaires :

Kotlin (Coroutines)

// Fonction suspendante en Kotlin
suspend fun getUserData(userId: String): UserData {
    try {
        // 'delay' suspend la coroutine sans bloquer le thread
        val user = userRepository.findById(userId) // fonction suspendante
        val posts = postRepository.findByUserId(userId) // fonction suspendante
        
        return UserData(user, posts)
    } catch (e: Exception) {
        throw CustomException("Erreur lors de la récupération", e)
    }
}

TypeScript (async/await)

// Fonction asynchrone en TypeScript
const getUserData = async (userId: string) => {
    try {
        // 'await' suspend l'exécution sans bloquer l'event loop
        const user = await User.findById(userId); // retourne une Promise
        const posts = await Post.find({ userId }); // retourne une Promise
        
        return { user, posts };
    } catch (error) {
        throw new AppError('Erreur lors de la récupération des données', 500);
    }
};

Correspondances conceptuelles :

  • suspend fun (Kotlin) ↔ async function (TypeScript)
  • delay() (Kotlin) ↔ await (TypeScript)
  • Coroutine (Kotlin) ↔ Promise (TypeScript)
  • Thread non-bloquant (Kotlin) ↔ Event Loop non-bloquant (Node.js)

Parallélisme :

Kotlin avec async

suspend fun getParallelData() = coroutineScope {
    val usersDeferred = async { userRepository.findAll() }
    val postsDeferred = async { postRepository.findAll() }
    
    // Attend que les deux opérations se terminent
    val users = usersDeferred.await()
    val posts = postsDeferred.await()
    
    return@coroutineScope Pair(users, posts)
}

TypeScript avec Promise.all

const getParallelData = async () => {
    // Exécution parallèle des deux requêtes
    const [users, posts] = await Promise.all([
        User.find(),
        Post.find()
    ]);
    
    return { users, posts };
};

Différences importantes :

  • Kotlin : Utilise des threads virtuels (coroutines) sur la JVM
  • Node.js : Utilise un event loop single-threaded avec des opérations I/O asynchrones
  • Kotlin : suspend fait partie du système de types
  • TypeScript : async/await est du sucre syntaxique au-dessus des Promises

Promise.all pour les opérations parallèles

Quand vous devez exécuter plusieurs opérations asynchrones indépendantes, utilisez Promise.all :

// ❌ Approche séquentielle (lente)
const users = await User.find(filter);
const totalUsers = await User.countDocuments(filter);
// Temps total : temps(users) + temps(totalUsers)

// ✅ Approche parallèle (rapide)
const [users, totalUsers] = await Promise.all([
    User.find(filter),
    User.countDocuments(filter)
]);
// Temps total : max(temps(users), temps(totalUsers))

Explication du code :

  • Promise.all exécute toutes les promesses en parallèle
  • Il attend que toutes soient résolues avant de continuer
  • La destructuration [users, totalUsers] récupère les résultats dans l'ordre
  • Si une promesse échoue, toutes les autres sont annulées

🛡️ Patterns d'Utilisation des Middlewares

Middleware d'Authentification

Appliquer à toutes les routes qui nécessitent une authentification utilisateur :

// Appliquer à toutes les routes du fichier
router.use(authMiddleware);

// Ou appliquer à des routes spécifiques
router.get('/protected-route', authMiddleware, asyncHandler(async (req, res) => {
    // Logique de route ici
}));

Contrôle d'Accès Basé sur les Rôles

Restreindre les routes à des rôles utilisateur spécifiques :

// Administrateurs seulement
router.post('/', adminMiddleware, createValidation, validateRequest, asyncHandler(async (req, res) => {
    // Seuls les administrateurs peuvent accéder à cette route
}));

// Enseignants ou Administrateurs
router.get('/stats', teacherMiddleware, asyncHandler(async (req, res) => {
    // Les enseignants et administrateurs peuvent accéder à cette route
}));

Validation des Requêtes

Valider les données entrantes avec express-validator :

const createValidation = [
    body('name')
        .notEmpty()
        .withMessage('Le nom est requis')
        .isLength({ max: 50 })
        .withMessage('Le nom doit faire moins de 50 caractères')
        .trim(),
    body('email')
        .isEmail()
        .normalizeEmail()
        .withMessage('Veuillez fournir un email valide'),
    body('capacity')
        .isInt({ min: 1 })
        .withMessage('La capacité doit être un entier positif')
];

router.post('/', createValidation, validateRequest, asyncHandler(async (req, res) => {
    // La validation est automatiquement gérée avant ce point
}));

✅ Validation des Entrées

Différence entre body, params et query

Les données peuvent arriver de trois façons différentes dans une requête HTTP :

1. body - Corps de la requête

Utilisé pour les données envoyées dans le corps de la requête (POST, PUT, PATCH) :

// URL : POST /api/users
// Corps JSON : { "name": "Jean", "email": "jean@example.com" }

const createValidation = [
    body('name')
        .notEmpty()
        .withMessage('Le nom est requis')
        .isLength({ max: 50 })
        .withMessage('Le nom doit faire moins de 50 caractères'),
    body('email')
        .isEmail()
        .withMessage('Veuillez fournir un email valide')
];

Explication : Les données body sont envoyées dans le corps de la requête HTTP, généralement en JSON. Elles sont invisibles dans l'URL et peuvent contenir de grandes quantités de données sensibles.

2. params - Paramètres d'URL

Utilisé pour les données dans le chemin de l'URL :

// URL : GET /api/users/64f123abc456def789
// params.id = "64f123abc456def789"

const userIdValidation = [
    param('id')
        .isMongoId()
        .withMessage('Format d\'ID utilisateur invalide')
];

router.get('/:id', userIdValidation, validateRequest, asyncHandler(async (req, res) => {
    const userId = req.params.id; // Récupère l'ID depuis l'URL
    const user = await User.findById(userId);
}));

Explication : Les paramètres sont définis dans la route avec :nomParam et représentent des parties variables de l'URL. Ils sont idéaux pour identifier des ressources spécifiques.

3. query - Paramètres de requête

Utilisé pour les données après le ? dans l'URL :

// URL : GET /api/users?page=2&limit=10&search=jean&role=admin
// query = { page: "2", limit: "10", search: "jean", role: "admin" }

const getUsersValidation = [
    query('page')
        .optional()
        .isInt({ min: 1 })
        .withMessage('La page doit être un entier positif'),
    query('limit')
        .optional()
        .isInt({ min: 1, max: 100 })
        .withMessage('La limite doit être entre 1 et 100'),
    query('search')
        .optional()
        .isLength({ min: 1, max: 50 })
        .withMessage('Le terme de recherche doit faire entre 1 et 50 caractères')
];

router.get('/', getUsersValidation, validateRequest, asyncHandler(async (req, res) => {
    // Récupération et conversion des paramètres de requête
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    const search = req.query.search as string;
}));

Explication : Les paramètres de requête sont utilisés pour filtrer, trier ou paginer les données. Ils sont visibles dans l'URL et optionnels la plupart du temps.

Règles de Validation Courantes

Validation d'ID MongoDB

const itemIdValidation = [
    param('id')
        .isMongoId()
        .withMessage('Format d\'ID d\'élément invalide')
];

Explication : Cette validation s'assure que l'ID dans l'URL est un ObjectId MongoDB valide (24 caractères hexadécimaux).

Paramètres de Requête

const getItemsValidation = [
    query('page')
        .optional()                    // Le paramètre est facultatif
        .isInt({ min: 1 })            // Doit être un entier ≥ 1
        .withMessage('La page doit être un entier positif'),
    query('limit')
        .optional()
        .isInt({ min: 1, max: 100 })  // Limité entre 1 et 100
        .withMessage('La limite doit être entre 1 et 100'),
    query('search')
        .optional()
        .isLength({ min: 1, max: 50 }) // Longueur de chaîne contrôlée
        .withMessage('Le terme de recherche doit faire entre 1 et 50 caractères')
];

Explication : Ces validations contrôlent les paramètres d'URL pour la pagination et la recherche, avec des limites raisonnables pour éviter les abus.

Validation du Corps de Requête

const createValidation = [
    body('name')
        .notEmpty()                    // Ne peut pas être vide
        .withMessage('Le nom est requis')
        .isLength({ max: 50 })        // Limite de longueur
        .withMessage('Le nom doit faire moins de 50 caractères')
        .trim(),                      // Supprime les espaces en début/fin
    body('email')
        .isEmail()                    // Validation de format email
        .normalizeEmail()             // Normalise le format (minuscules, etc.)
        .withMessage('Veuillez fournir un email valide'),
    body('roles')
        .optional()                   // Champ facultatif
        .isArray()                    // Doit être un tableau
        .withMessage('Les rôles doivent être un tableau')
        .custom((roles) => {          // Validation personnalisée
            const validRoles = ['student', 'teacher', 'admin'];
            return roles.every((role: string) => validRoles.includes(role));
        })
        .withMessage('Les rôles doivent contenir des valeurs valides : student, teacher, admin')
];

Explication :

  • trim() nettoie les espaces parasites
  • normalizeEmail() standardise le format des emails
  • custom() permet des validations spécifiques métier
  • every() vérifie que tous les éléments du tableau sont valides

Validation Personnalisée

body('maxCapacity')
    .optional()
    .isInt({ min: 0 })
    .withMessage('La capacité maximale doit être un entier non négatif')
    .custom((value, { req }) => {
        // Accès aux autres champs de la requête via req.body
        if (req.body.minCapacity && parseInt(value) < parseInt(req.body.minCapacity)) {
            throw new Error('La capacité maximale doit être supérieure à la capacité minimale');
        }
        return true; // Validation réussie
    })

Explication : Les validations custom permettent des règles complexes qui dépendent de plusieurs champs ou de logique métier spécifique.

🏗️ Opérations CRUD

Routes GET (Opérations de Lecture)

Récupérer tous les éléments avec pagination

router.get('/', getItemsValidation, validateRequest, asyncHandler(async (req: Request, res: Response) => {
    // Récupération et conversion des paramètres de requête avec valeurs par défaut
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    const search = req.query.search as string;
    
    // Construction de l'objet filtre pour MongoDB
    const filter: any = {};
    
    if (search) {
        // Recherche insensible à la casse dans plusieurs champs
        filter.$or = [
            { name: { $regex: search, $options: 'i' } },
            { code: { $regex: search, $options: 'i' } }
        ];
    }
    
    // Construction de l'objet tri
    const sortBy = (req.query.sortBy as string) || 'createdAt';
    const sortOrder = (req.query.sortOrder as string) || 'desc';
    const sort: any = {};
    sort[sortBy] = sortOrder === 'asc' ? 1 : -1; // 1 = croissant, -1 = décroissant
    
    // Calcul du nombre d'éléments à ignorer pour la pagination
    const skip = (page - 1) * limit;
    
    // Exécution des requêtes en parallèle pour optimiser les performances
    const [items, totalItems] = await Promise.all([
        Model.find(filter)
            .sort(sort)
            .skip(skip)           // Ignorer les éléments des pages précédentes
            .limit(limit)         // Limiter le nombre de résultats
            .select('-__v'),      // Exclure le champ version de MongoDB
        Model.countDocuments(filter) // Compter le total pour la pagination
    ]);
    
    // Calcul des informations de pagination
    const totalPages = Math.ceil(totalItems / limit);
    
    res.json({
        success: true,
        data: {
            items,
            pagination: {
                currentPage: page,
                totalPages,
                totalItems,
                limit,
                hasNextPage: page < totalPages,
                hasPrevPage: page > 1
            }
        }
    });
}));

Explication détaillée :

  • Paramètres de requête : page, limit, search sont extraits de l'URL
  • Filtrage : L'objet filter construit une requête MongoDB avec recherche optionnelle
  • Tri : L'objet sort définit l'ordre des résultats (1 = croissant, -1 = décroissant)
  • Pagination : skip calcule combien d'éléments ignorer, limit définit la taille de page
  • Promise.all : Exécute la requête de données et le comptage en parallèle pour de meilleures performances
  • Métadonnées : La réponse inclut les informations de pagination pour faciliter la navigation côté client

Récupérer un élément par ID

router.get('/:id', itemIdValidation, validateRequest, asyncHandler(async (req: Request, res: Response) => {
    // Récupération de l'ID depuis les paramètres d'URL
    const item = await Model.findById(req.params.id).select('-__v');
    
    // Vérification de l'existence de l'élément
    if (!item) {
        throw new AppError('Élément non trouvé', 404);
    }
    
    res.json({
        success: true,
        data: { item }
    });
}));

Explication :

  • req.params.id récupère l'ID depuis l'URL (ex: /api/items/64f123abc456def789)
  • findById() recherche l'élément par son ID MongoDB
  • select('-__v') exclut le champ version technique de MongoDB
  • L'erreur 404 est lancée si l'élément n'existe pas

Routes POST (Opérations de Création)

router.post('/', adminMiddleware, createValidation, validateRequest, asyncHandler(async (req: Request, res: Response) => {
    // Extraction des données validées du corps de la requête
    const { name, code, capacity } = req.body;
    
    // Vérification des doublons avant création
    const existingItem = await Model.findOne({ code });
    if (existingItem) {
        throw new AppError('Un élément avec ce code existe déjà', 400);
    }
    
    // Création du nouvel élément
    const item = new Model({
        name,
        code,
        capacity
    });
    
    // Sauvegarde en base de données
    await item.save();
    
    // Réponse avec code 201 (Created)
    res.status(201).json({
        success: true,
        message: 'Élément créé avec succès',
        data: {
            item: {
                id: item._id,
                name: item.name,
                code: item.code,
                capacity: item.capacity,
                createdAt: item.createdAt,
                updatedAt: item.updatedAt
            }
        }
    });
}));

Explication détaillée :

  • Middleware de sécurité : adminMiddleware vérifie que l'utilisateur a les droits administrateur
  • Validation : createValidation et validateRequest s'assurent que les données sont valides
  • Vérification de doublons : Recherche d'éléments existants avec le même code
  • Instanciation : new Model() crée une instance mais ne la sauvegarde pas encore
  • Sauvegarde : save() persiste l'élément en base de données
  • Réponse structurée : Retourne les données créées avec un statut 201 (Created)

Routes PUT (Opérations de Mise à Jour)

router.put('/:id', adminMiddleware, [...itemIdValidation, ...updateValidation], validateRequest, asyncHandler(async (req: Request, res: Response) => {
    const { name, code, capacity } = req.body;
    
    // Recherche de l'élément à modifier
    const item = await Model.findById(req.params.id);
    if (!item) {
        throw new AppError('Élément non trouvé', 404);
    }
    
    // Vérification des doublons de code (en excluant l'élément actuel)
    if (code && code !== item.code) {
        const existingCode = await Model.findOne({ 
            code, 
            _id: { $ne: req.params.id } // Exclure l'élément actuel
        });
        if (existingCode) {
            throw new AppError('Un élément avec ce code existe déjà', 400);
        }
    }
    
    // Mise à jour conditionnelle des champs
    if (name !== undefined) item.name = name;
    if (code !== undefined) item.code = code;
    if (capacity !== undefined) item.capacity = capacity;
    
    // Sauvegarde des modifications
    await item.save();
    
    res.json({
        success: true,
        message: 'Élément mis à jour avec succès',
        data: { item }
    });
}));

Explication :

  • Validation combinée : [...itemIdValidation, ...updateValidation] combine plusieurs règles de validation
  • Vérification d'existence : S'assure que l'élément à modifier existe
  • Gestion des doublons : $ne (not equal) exclut l'élément actuel de la recherche de doublons
  • Mise à jour partielle : Seuls les champs fournis sont modifiés
  • Sauvegarde intelligente : Mongoose ne sauvegarde que les champs modifiés

Routes DELETE (Opérations de Suppression)

router.delete('/:id', adminMiddleware, itemIdValidation, validateRequest, asyncHandler(async (req: Request, res: Response) => {
    // Recherche de l'élément avant suppression (pour la réponse)
    const item = await Model.findById(req.params.id);
    
    if (!item) {
        throw new AppError('Élément non trouvé', 404);
    }
    
    // Suppression de l'élément
    await Model.findByIdAndDelete(req.params.id);
    
    res.json({
        success: true,
        message: 'Élément supprimé avec succès',
        data: {
            deletedItem: {
                id: item._id,
                name: item.name,
                code: item.code
            }
        }
    });
}));

Explication :

  • Récupération avant suppression : Nécessaire pour retourner les informations de l'élément supprimé
  • findByIdAndDelete() : Supprime l'élément en une seule opération
  • Réponse informative : Indique quelles données ont été supprimées

📋 Documentation des Routes

Format de Commentaires Standardisé

Chaque route doit être documentée avec un commentaire standardisé qui précise :

  • @route : La méthode HTTP et l'URL complète
  • @desc : Description claire de ce que fait la route
  • @access : Niveau d'accès requis

Format général :

// @route   [MÉTHODE] [URL_COMPLÈTE]
// @desc    [Description de la fonctionnalité]
// @access  [Niveau d'accès]

Exemples de Documentation

Route publique (sans authentification)

// @route   POST /api/accounts/register
// @desc    Register a new user
// @access  Public
router.post('/register', registerValidation, validateRequest, asyncHandler(async (req, res) => {
    // Logique d'inscription
}));

Route privée (authentification requise)

// @route   GET /api/accounts/me
// @desc    Get current user profile
// @access  Private
router.get('/me', authMiddleware, asyncHandler(async (req, res) => {
    // Logique pour récupérer le profil utilisateur
}));

Route administrateur uniquement

// @route   GET /api/users
// @desc    Get all users with pagination and filters
// @access  Private (Admin only)
router.get('/', adminMiddleware, getUsersValidation, validateRequest, asyncHandler(async (req, res) => {
    // Logique pour récupérer tous les utilisateurs
}));

Route avec paramètres

// @route   GET /api/users/:id
// @desc    Get user by ID
// @access  Private (Admin only)
router.get('/:id', adminMiddleware, userIdValidation, validateRequest, asyncHandler(async (req, res) => {
    // Logique pour récupérer un utilisateur spécifique
}));

Route de mise à jour

// @route   PUT /api/users/:id/roles
// @desc    Update user roles
// @access  Private (Admin only)
router.put('/:id/roles', adminMiddleware, updateRolesValidation, validateRequest, asyncHandler(async (req, res) => {
    // Logique pour mettre à jour les rôles
}));

Route de recherche

// @route   GET /api/users/search/:term
// @desc    Search users by email, name, or department
// @access  Private
router.get('/search/:term', authMiddleware, searchValidation, validateRequest, asyncHandler(async (req, res) => {
    // Logique de recherche
}));

Conventions pour @access

Niveaux d'accès standards :

  • Public - Accessible sans authentification
  • Private - Authentification requise
  • Private (Admin only) - Administrateurs uniquement
  • Private (Teacher or Admin) - Enseignants et administrateurs
  • Private (Owner or Admin) - Propriétaire de la ressource ou administrateur

Exemples d'accès spécialisés :

// @route   GET /api/course-units/stats
// @desc    Get course unit statistics
// @access  Private (Admin only)

// @route   PUT /api/accounts/profile
// @desc    Update user profile
// @access  Private (Owner only)

// @route   DELETE /api/users/:id
// @desc    Delete user account
// @access  Private (Admin only, cannot delete self)

Pourquoi cette documentation est importante :

  • Clarté : Identifie rapidement le rôle de chaque route
  • Sécurité : Explicite les niveaux d'accès requis
  • Maintenance : Facilite la compréhension pour les futurs développeurs
  • Tests : Aide à identifier quels cas de test sont nécessaires
  • Documentation API : Peut être extraite pour générer une documentation automatique

🚨 Gestion des Erreurs

Utilisation d'AppError

// 404 Non Trouvé
if (!item) {
    throw new AppError('Élément non trouvé', 404);
}

// 400 Requête Incorrecte
if (existingItem) {
    throw new AppError('Un élément avec ce code existe déjà', 400);
}

// 403 Accès Interdit
if (item.userId !== req.user?.userId) {
    throw new AppError('Accès refusé. Vous ne pouvez modifier que vos propres éléments.', 403);
}

Explication : AppError est une classe personnalisée qui standardise les erreurs avec un message et un code de statut HTTP approprié.

Gestion des Erreurs Asynchrones

router.get('/', asyncHandler(async (req: Request, res: Response) => {
    // Toutes les erreurs lancées sont automatiquement capturées
    // et transmises au middleware de gestion d'erreur
    const data = await Model.find();
    res.json({ success: true, data });
}));

Explication : asyncHandler est un wrapper qui capture automatiquement les erreurs des fonctions async et les transmet au middleware d'erreur Express.

📤 Standards de Format de Réponse

Réponses de Succès

// Réponse d'élément unique
res.json({
    success: true,
    data: { item }
});

// Multiples éléments avec pagination
res.json({
    success: true,
    data: {
        items,
        pagination: {
            currentPage: page,
            totalPages,
            totalItems,
            limit,
            hasNextPage: page < totalPages,
            hasPrevPage: page > 1
        }
    }
});

// Réponse de création
res.status(201).json({
    success: true,
    message: 'Élément créé avec succès',
    data: { item }
});

// Réponse de mise à jour/suppression
res.json({
    success: true,
    message: 'Opération terminée avec succès',
    data: { item }
});

Explication : Toutes les réponses suivent une structure cohérente avec success: true, un data optionnel, et un message optionnel pour les opérations de modification.

🔍 Patterns de Routes Avancés

Routes de Recherche

router.get('/search/:term', [
    param('term')
        .isLength({ min: 1, max: 50 })
        .withMessage('Le terme de recherche doit faire entre 1 et 50 caractères')
], validateRequest, asyncHandler(async (req: Request, res: Response) => {
    const searchTerm = req.params.term;
    const limit = parseInt(req.query.limit as string) || 10;
    
    // Recherche avec regex insensible à la casse
    const items = await Model.find({
        $or: [
            { name: { $regex: searchTerm, $options: 'i' } },      // 'i' = insensible à la casse
            { code: { $regex: searchTerm, $options: 'i' } }
        ]
    })
    .limit(limit)                    // Limitation du nombre de résultats
    .select('name code createdAt')   // Sélection de champs spécifiques
    .sort({ name: 1 });             // Tri alphabétique
    
    res.json({
        success: true,
        data: {
            items,
            count: items.length
        }
    });
}));

Explication :

  • $regex avec option i permet une recherche insensible à la casse
  • $or recherche dans plusieurs champs simultanément
  • select() optimise la requête en ne récupérant que les champs nécessaires
  • sort({ name: 1 }) trie par nom en ordre croissant

Routes de Statistiques

router.get('/stats', adminMiddleware, asyncHandler(async (req: Request, res: Response) => {
    // Pipeline d'agrégation MongoDB pour calculer des statistiques
    const stats = await Model.aggregate([
        {
            $group: {
                _id: null,                                    // Pas de groupement spécifique
                totalItems: { $sum: 1 },                    // Compte total
                averageCapacity: { $avg: '$capacity' },      // Moyenne
                maxCapacity: { $max: '$capacity' },          // Maximum
                minCapacity: { $min: '$capacity' }           // Minimum
            }
        }
    ]);
    
    // Requête séparée pour les éléments récents
    const recentItems = await Model.find()
        .sort({ createdAt: -1 })     // Tri par date de création décroissante
        .limit(5)                    // Limité aux 5 plus récents
        .select('name code capacity createdAt');
    
    // Gestion du cas où aucune donnée n'existe
    const result = stats[0] || {
        totalItems: 0,
        averageCapacity: 0,
        maxCapacity: 0,
        minCapacity: 0
    };
    
    res.json({
        success: true,
        data: {
            overview: result,
            recentItems
        }
    });
}));

Explication :

  • aggregate() utilise le framework d'agrégation MongoDB pour des calculs complexes
  • $group groupe les documents et applique des opérations comme $sum, $avg
  • _id: null signifie qu'on groupe tous les documents ensemble
  • Le fallback stats[0] || {...} gère le cas où la collection est vide

Routes Conditionnelles

router.get('/by-role/:role', adminMiddleware, [
    param('role')
        .isIn(['student', 'teacher', 'admin'])
        .withMessage('Le rôle doit être student, teacher, ou admin')
], validateRequest, asyncHandler(async (req: Request, res: Response) => {
    const role = req.params.role;
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    
    // Recherche avec critères multiples
    const users = await User.find({ 
        roles: { $in: [role] },      // Le tableau 'roles' contient le rôle recherché
        isActive: true               // Utilisateurs actifs seulement
    })
    .skip((page - 1) * limit)       // Pagination
    .limit(limit)
    .select('email firstName lastName department roles');
    
    res.json({
        success: true,
        data: { users }
    });
}));

Explication :

  • $in vérifie si le rôle recherché est présent dans le tableau roles
  • La validation isIn() s'assure que seuls les rôles valides sont acceptés
  • La combinaison de filtres permet des requêtes précises

📝 Ajouter des Routes au Serveur

1. Créer le Fichier de Routes

Créez votre fichier de routes dans /server/src/routes/ :

// /server/src/routes/my-feature-routes.ts
import { Router } from 'express';
// ... imports et définitions des routes
export default router;

Explication : Chaque fonctionnalité a son propre fichier de routes pour maintenir une organisation claire et modulaire.

2. Enregistrer les Routes dans le Serveur Principal

Ajoutez vos routes dans /server/src/index.ts :

// Import de vos routes
import myFeatureRoutes from './routes/my-feature-routes';

// Dans la méthode initializeRoutes()
private initializeRoutes(): void {
    // ... routes existantes
    this.app.use('/api/my-feature', myFeatureRoutes);
    
    // Mise à jour de l'endpoint racine pour inclure vos nouvelles routes
    this.app.get('/', (req, res) => {
        res.json({
            message: 'Serveur API',
            endpoints: {
                // ... endpoints existants
                myFeature: '/api/my-feature'
            }
        });
    });
}

Explication :

  • this.app.use() monte les routes sur un chemin spécifique
  • Le préfixe /api/my-feature sera automatiquement ajouté à toutes les routes du fichier
  • L'endpoint racine documente tous les chemins disponibles

🎯 Bonnes Pratiques

Organisation des Routes

  • Grouper les fonctionnalités liées dans le même fichier de routes
  • Utiliser des noms de routes descriptifs qui indiquent clairement leur objectif
  • Ordonner les routes logiquement : routes générales d'abord, puis spécifiques
  • Utiliser des patterns d'URL cohérents à travers toutes les fonctionnalités

Exemple d'organisation :

// Routes générales d'abord
router.get('/', getAllItems);           // GET /api/items
router.post('/', createItem);           // POST /api/items

// Routes spécifiques ensuite
router.get('/stats', getStats);         // GET /api/items/stats
router.get('/search/:term', search);    // GET /api/items/search/terme

// Routes avec paramètres à la fin
router.get('/:id', getItemById);        // GET /api/items/123
router.put('/:id', updateItem);         // PUT /api/items/123
router.delete('/:id', deleteItem);      // DELETE /api/items/123

Utilisation des Middlewares

  • Appliquer l'authentification globalement quand la plupart des routes en ont besoin
  • Utiliser les middlewares de rôle stratégiquement pour les fonctions administratives
  • Valider toutes les entrées avant traitement
  • Gérer les erreurs de manière cohérente avec asyncHandler et AppError

Considérations de Performance

  • Utiliser des index de base de données pour les champs fréquemment interrogés
  • Limiter les ensembles de résultats avec la pagination
  • Sélectionner uniquement les champs nécessaires pour réduire le transfert de données
  • Utiliser des pipelines d'agrégation pour les requêtes complexes

Exemple d'optimisation :

// ❌ Requête non optimisée
const users = await User.find().populate('posts').populate('comments');

// ✅ Requête optimisée
const users = await User.find({ isActive: true })
    .select('email firstName lastName department')  // Champs spécifiques seulement
    .limit(50)                                     // Limitation des résultats
    .sort({ createdAt: -1 });                     // Tri sur un champ indexé

Sécurité

  • Valider toutes les entrées utilisateur de manière approfondie
  • Utiliser des requêtes paramétrées pour prévenir les attaques par injection
  • Implémenter des contrôles d'accès appropriés avec les middlewares de rôles
  • Ne pas exposer de données sensibles dans les réponses API

Exemple de sécurisation :

// Validation stricte des rôles
const updateUserValidation = [
    body('roles')
        .isArray()
        .custom((roles, { req }) => {
            // Vérifier que l'utilisateur ne peut pas s'auto-promouvoir admin
            if (roles.includes('admin') && !req.user?.roles.includes('admin')) {
                throw new Error('Seuls les administrateurs peuvent assigner le rôle admin');
            }
            return true;
        })
];

// Contrôle d'accès basé sur la propriété
router.put('/:id', authMiddleware, asyncHandler(async (req, res) => {
    const item = await Model.findById(req.params.id);
    
    // Vérifier que l'utilisateur possède la ressource ou est admin
    if (item.ownerId !== req.user?.userId && !req.user?.roles.includes('admin')) {
        throw new AppError('Accès refusé', 403);
    }
    
    // ... logique de mise à jour
}));

Cette approche basée sur des patterns assure la cohérence à travers votre API, facilite la maintenance, et fournit une base solide pour construire des routes robustes et évolutives.

Clone this wiki locally