-
Notifications
You must be signed in to change notification settings - Fork 0
Créer des Routes dans le Serveur Express
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.
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 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';
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 !
};
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
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.
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
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
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
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
}));
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
}));
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
}));
Les données peuvent arriver de trois façons différentes dans une requête HTTP :
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.
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.
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.
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.
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
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
etvalidateRequest
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)
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
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
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]
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
}));
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
// 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é.
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.
// 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.
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 optioni
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
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
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 tableauroles
- La validation
isIn()
s'assure que seuls les rôles valides sont acceptés - La combinaison de filtres permet des requêtes précises
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.
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
- 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
- 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
- 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é
- 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.