AzDev

III - Les Middlewares dans ExpressJS

ExpressJS ouvre un monde de possibilités pour le développement web avec Node.js. Sa simplicité apparente cache une grande puissance et flexibilité qui vous permettront de créer des applications web modernes, performantes et maintenables. Que vous souhaitiez développer des APIs, des sites web ou des applications temps réel, les compétences que vous acquerrez dans ce cours vous seront précieuses.

Publié le
III - Les Middlewares dans ExpressJS

1. Concept et fonctionnement des middlewares

Définition et rôle des middlewares

Les middlewares sont l'un des concepts les plus puissants et fondamentaux d'ExpressJS. Un middleware est une fonction qui a accès aux objets de requête (req), de réponse (res), et à la fonction suivante dans le cycle de requête-réponse de l'application, généralement appelée next.

Les middlewares peuvent :

  • Exécuter n'importe quel code
  • Modifier les objets de requête et de réponse
  • Terminer le cycle de requête-réponse
  • Appeler le prochain middleware dans la pile

La structure d'une fonction middleware est la suivante :

1function monMiddleware(req, res, next) {
2  // Code du middleware
3  
4  // Appel du middleware suivant
5  next();
6}

Les middlewares sont au cœur de la philosophie d'Express, qui peut être considéré comme une série d'appels de fonctions middleware.

Cycle de vie d'une requête

Lorsqu'une requête arrive sur un serveur Express, elle traverse une série de middlewares avant de recevoir une réponse. Ce processus peut être visualisé comme une "pile" ou une "chaîne" de middlewares.

Voici les étapes typiques du cycle de vie d'une requête :

  1. Réception de la requête : Express reçoit la requête HTTP
  2. Middlewares d'application : La requête traverse les middlewares globaux (définis avec app.use())
  3. Middlewares de routeur : Si la requête correspond à un routeur, elle traverse ses middlewares
  4. Middlewares de route : Si la requête correspond à une route spécifique, elle traverse ses middlewares
  5. Gestionnaire de route : La fonction finale qui génère une réponse
  6. Middlewares d'erreur : Si une erreur est générée, elle est traitée par les middlewares d'erreur
  7. Envoi de la réponse : La réponse est envoyée au client

Si un middleware ne termine pas le cycle de requête-réponse (en appelant res.send(), res.json(), etc.), il doit appeler next() pour passer au middleware suivant, sinon la requête restera en suspens.

Ordre d'exécution des middlewares

L'ordre dans lequel les middlewares sont définis est crucial, car ils sont exécutés séquentiellement. Les middlewares définis en premier sont exécutés en premier.

1const express = require('express');
2const app = express();
3
4// Premier middleware - s'exécute pour toutes les requêtes
5app.use((req, res, next) => {
6  console.log('Middleware 1');
7  next();
8});
9
10// Deuxième middleware - s'exécute pour toutes les requêtes
11app.use((req, res, next) => {
12  console.log('Middleware 2');
13  next();
14});
15
16// Route spécifique avec son propre middleware
17app.get('/', (req, res, next) => {
18  console.log('Middleware de route');
19  res.send('Hello World');
20});
21
22// Pour une requête GET à '/', la console affichera :
23// Middleware 1
24// Middleware 2
25// Middleware de route

Les middlewares peuvent également être limités à des chemins spécifiques :

1// Ce middleware ne s'applique qu'aux requêtes commençant par /api
2app.use('/api', (req, res, next) => {
3  console.log('API Middleware');
4  next();
5});

Passage au middleware suivant (next())

La fonction next() est un paramètre fourni aux fonctions middleware et, lorsqu'elle est appelée, exécute le middleware suivant dans la pile. Si next() n'est pas appelé, la requête reste en suspens et le client n'obtient jamais de réponse.

next() peut être appelé de différentes manières :

  1. Appel simple : next() - Passe au middleware suivant
  2. Avec une erreur : next(err) - Saute tous les middlewares restants et passe à un middleware de gestion d'erreur
  3. Avec 'route' : next('route') - Saute le reste des middlewares dans la route actuelle et passe à la route suivante

Exemple d'utilisation de next('route') :

1app.get('/utilisateurs/:id', 
2  // Premier middleware
3  (req, res, next) => {
4    // Si l'ID est 0, passer à la route suivante
5    if (req.params.id === '0') {
6      next('route');
7    } else {
8      next();
9    }
10  },
11  // Deuxième middleware (ignoré si ID est 0)
12  (req, res, next) => {
13    res.send('Utilisateur régulier');
14  }
15);
16
17// Route suivante (exécutée si ID est 0)
18app.get('/utilisateurs/:id', (req, res) => {
19  res.send('Utilisateur spécial');
20});

2. Middlewares intégrés

Express fournit plusieurs middlewares intégrés qui simplifient les tâches courantes.

express.json()

Ce middleware analyse les corps de requête JSON entrants et les rend disponibles dans req.body. Il est basé sur body-parser.

1const express = require('express');
2const app = express();
3
4// Analyse les requêtes avec Content-Type: application/json
5app.use(express.json());
6
7app.post('/api/data', (req, res) => {
8  console.log(req.body); // Contient les données JSON parsées
9  res.json({ reçu: true });
10});

Options configurables :

  • limit : Contrôle la taille maximale du corps de requête (par défaut '100kb')
  • strict : N'accepte que les tableaux et objets (par défaut true)
  • reviver : Fonction passée à JSON.parse()

express.urlencoded()

Ce middleware analyse les corps de requête URL-encoded (typiquement envoyés par les formulaires HTML) et les rend disponibles dans req.body.

1// Analyse les requêtes avec Content-Type: application/x-www-form-urlencoded
2app.use(express.urlencoded({ extended: false }));
3
4app.post('/formulaire', (req, res) => {
5  console.log(req.body); // Contient les données du formulaire
6  res.send('Formulaire reçu');
7});

Options importantes :

  • extended : Utilise la bibliothèque qs (true) ou querystring (false) pour l'analyse
  • limit : Contrôle la taille maximale du corps de requête

express.static()

Ce middleware sert des fichiers statiques comme des images, des fichiers CSS et JavaScript.

1// Sert les fichiers statiques du répertoire 'public'
2app.use(express.static('public'));
3
4// Avec un préfixe de chemin
5app.use('/static', express.static('public'));
6// Accessible via /static/css/style.css pour le fichier public/css/style.css

Options utiles :

  • dotfiles : Comment gérer les fichiers commençant par un point (par défaut 'ignore')
  • etag : Activer ou désactiver la génération d'etag (par défaut true)
  • extensions : Définir les extensions de secours (par défaut false)
  • index : Envoyer un fichier d'index si un répertoire est demandé (par défaut 'index.html')
  • maxAge : Définir l'en-tête Cache-Control en millisecondes ou une chaîne (par défaut 0)

Autres middlewares intégrés

Express 4.x et 5.x incluent d'autres middlewares utiles :

  • express.raw() : Analyse les corps de requête bruts dans un Buffer
  • express.text() : Analyse les corps de requête texte
  • express.Router() : Crée un nouvel objet routeur

3. Middlewares tiers populaires

L'écosystème Express comprend de nombreux middlewares tiers qui étendent ses fonctionnalités.

morgan (logging)

Morgan est un middleware de logging HTTP qui enregistre les détails des requêtes.

1const express = require('express');
2const morgan = require('morgan');
3const app = express();
4
5// Utilisation de morgan avec le format prédéfini 'dev'
6app.use(morgan('dev'));
7// Exemple de sortie: GET / 200 7.957 ms - 13
8
9// Formats prédéfinis disponibles : combined, common, dev, short, tiny

Morgan peut également être configuré pour écrire dans un fichier :

1const fs = require('fs');
2const path = require('path');
3
4// Création d'un flux d'écriture
5const accessLogStream = fs.createWriteStream(
6  path.join(__dirname, 'access.log'), 
7  { flags: 'a' }
8);
9
10// Configuration de morgan pour écrire dans ce flux
11app.use(morgan('combined', { stream: accessLogStream }));

cors (Cross-Origin Resource Sharing)

Le middleware CORS permet de gérer le partage de ressources entre origines multiples.

1const express = require('express');
2const cors = require('cors');
3const app = express();
4
5// Activer CORS pour toutes les routes
6app.use(cors());
7
8// Configuration avancée
9app.use(cors({
10  origin: 'https://example.com', // Autoriser seulement cette origine
11  methods: ['GET', 'POST'],      // Méthodes autorisées
12  allowedHeaders: ['Content-Type', 'Authorization'], // En-têtes autorisés
13  credentials: true              // Autoriser les cookies
14}));
15
16// CORS pour une route spécifique
17app.get('/api/public', cors(), (req, res) => {
18  res.json({ message: 'Cette API est accessible de partout' });
19});

compression (compression de réponses)

Ce middleware compresse les réponses envoyées aux clients, réduisant la taille des données transmises.

1const express = require('express');
2const compression = require('compression');
3const app = express();
4
5// Compression de base
6app.use(compression());
7
8// Configuration avancée
9app.use(compression({
10  level: 6,                    // Niveau de compression (0-9, par défaut 6)
11  threshold: 1024,             // Seuil minimal en octets pour la compression
12  filter: (req, res) => {      // Fonction pour déterminer quand compresser
13    if (req.headers['x-no-compression']) {
14      return false;            // Ne pas compresser si cet en-tête est présent
15    }
16    return compression.filter(req, res); // Utiliser le filtre par défaut
17  }
18}));

4. Création de middlewares personnalisés

Créer vos propres middlewares vous permet d'encapsuler des fonctionnalités spécifiques à votre application.

Structure d'un middleware

Un middleware est une fonction qui prend trois paramètres : req, res, et next.

1function monMiddleware(req, res, next) {
2  // 1. Exécuter du code
3  console.log('Middleware exécuté à', new Date().toISOString());
4  
5  // 2. Modifier les objets req et res
6  req.horodatage = Date.now();
7  
8  // 3. Terminer le cycle de requête-réponse OU
9  // if (condition) {
10  //   return res.status(401).send('Non autorisé');
11  // }
12  
13  // 4. Appeler le prochain middleware
14  next();
15}
16
17// Utilisation
18app.use(monMiddleware);

Middlewares d'application

Les middlewares d'application sont montés sur l'instance de l'application avec app.use() et s'appliquent à toutes les requêtes ou à celles correspondant à un chemin spécifique.

1// Middleware de logging personnalisé
2app.use((req, res, next) => {
3  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
4  next();
5});
6
7// Middleware pour un chemin spécifique
8app.use('/api', (req, res, next) => {
9  console.log('Requête API reçue');
10  next();
11});

Middlewares de routeur

Les middlewares de routeur fonctionnent de la même manière que les middlewares d'application, mais sont liés à une instance de express.Router().

1const express = require('express');
2const router = express.Router();
3
4// Middleware spécifique au routeur
5router.use((req, res, next) => {
6  console.log('Routeur utilisateurs accédé');
7  next();
8});
9
10router.get('/', (req, res) => {
11  res.send('Liste des utilisateurs');
12});
13
14// Montage du routeur
15app.use('/utilisateurs', router);

Middlewares d'erreur

Les middlewares d'erreur sont spéciaux car ils prennent quatre paramètres : err, req, res, et next.

1// Middleware de gestion d'erreur
2app.use((err, req, res, next) => {
3  console.error(err.stack);
4  res.status(500).send('Quelque chose s\'est mal passé!');
5});

Pour qu'un middleware d'erreur soit appelé, un autre middleware doit passer une erreur à next() :

1app.get('/problematique', (req, res, next) => {
2  try {
3    // Code qui pourrait générer une erreur
4    const resultat = fonctionProblematique();
5    res.send(resultat);
6  } catch (err) {
7    next(err); // Passe l'erreur au middleware d'erreur
8  }
9});
10
11// Ou plus simplement
12app.get('/problematique', (req, res, next) => {
13  // Si next() est appelé avec un argument, Express le traite comme une erreur
14  next(new Error('Quelque chose s\'est mal passé'));
15});

Exercices pratiques

Exercice 1: Créer un middleware de logging personnalisé

Créez un middleware qui enregistre les détails de chaque requête (méthode, URL, heure, adresse IP) dans un fichier de log.

Exercice 2: Implémenter un middleware d'authentification simple

Créez un middleware qui vérifie si une requête contient un token valide dans les en-têtes et refuse l'accès si ce n'est pas le cas.

Exercice 3: Intégrer plusieurs middlewares tiers

Configurez une application Express avec plusieurs middlewares tiers (morgan, cors, compression) et assurez-vous qu'ils sont exécutés dans le bon ordre.

Ressources supplémentaires