Aller au contenu

Les middlewares dans Express

Certaines responsabilités reviennent dans de nombreuses requêtes sans appartenir directement au domaine de l’application :

  • journaliser les requêtes;
  • lire un corps JSON;
  • vérifier l’identité de l’utilisateur;
  • contrôler ses droits;
  • valider les données reçues;
  • charger une ressource demandée.

On pourrait effectuer chacune de ces opérations directement dans les gestionnaires de route. Mais la même logique serait alors répétée à plusieurs endroits.

Express permet plutôt de placer ces traitements dans des middlewares, c’est-à-dire des étapes intermédiaires exécutées pendant le traitement d’une requête.

Un middleware intercepte une requête, effectue une responsabilité précise, puis décide si le traitement doit continuer.

Un middleware Express est une fonction qui reçoit généralement trois paramètres (req, res, next) :

function journalisation(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}

On peut ensuite le monter dans l’application :

app.use(journalisation);

Les trois paramètres ont des rôles distincts :

  • req représente la requête;
  • res représente la réponse qui sera envoyée au client;
  • next permet de transférer le traitement à l’étape suivante.

La requête peut ainsi traverser plusieurs middlewares avant d’atteindre le gestionnaire de route.

Une requête traverse plusieurs étapes avant d'atteindre le gestionnaire

À chaque exécution, un middleware doit éventuellement mener à l’une de ces trois issues :

  1. appeler next() pour continuer vers l’étape suivante;
  2. produire une réponse avec res.send(), res.json(), res.end(), etc.;
  3. transmettre une erreur avec next(err).

Par exemple :

function verifierCle(req, res, next) {
if (!req.headers["x-api-key"]) {
return res.status(401).json({
erreur: "clé manquante",
});
}
next();
}

Si la clé est absente, le middleware produit lui-même la réponse.

Sinon, next() laisse la requête poursuivre son chemin.

Un middleware peut être appliqué à plusieurs niveaux.

app.use(journalisation);
app.use("/admin", authentifier);
app.get("/profil", authentifier, afficherProfil);

Les fonctions fournies par Express peuvent elles aussi être des middlewares.

Par exemple :

app.use(express.json());

express.json() lit un éventuel corps JSON et rend son contenu disponible dans req.body.

Dans Express, l’ordre de déclaration détermine l’ordre d’exécution.

Considérons cette route :

app.get(
"/boites/:id",
journalisation,
verifierId,
chargerBoite,
afficherBoite
);

Pour une requête correspondante, Express essaie les étapes de gauche à droite :

journalisation
↓
verifierId
↓
chargerBoite
↓
afficherBoite

Chaque middleware appelle next() lorsqu’il souhaite poursuivre.

Un middleware peut cependant décider de terminer la chaîne en produisant une réponse.

Par exemple :

function verifierId(req, res, next) {
if (!idValide(req.params.id)) {
return res.status(400).json({
erreur: "identifiant invalide",
});
}
next();
}

Si l’identifiant est invalide, chargerBoite et afficherBoite ne seront jamais exécutés.

Un middleware peut rencontrer une erreur qu’il ne souhaite pas transformer directement en réponse.

Il peut alors appeler :

next(err);

Express cherche ensuite un middleware de gestion d’erreurs.

Celui-ci se distingue par quatre paramètres :

app.use((err, req, res, next) => {
console.error(err);
res.status(err.status ?? 500).json({
erreur: err.message,
});
});

Ce middleware est généralement déclaré après les routes et les autres middlewares.

Avec Express 5, une erreur levée dans un middleware async ou une promesse rejetée est automatiquement transmise au traitement des erreurs.

Par exemple :

app.get("/boites/:id", async (req, res) => {
const boite = await Boite.findById(req.params.id);
res.json(boite);
});

Si findById() échoue en levant une erreur, Express peut transmettre cette erreur au middleware de gestion d’erreurs.

Le fonctionnement des middlewares peut être compris comme un pipeline de traitement.

Chaque étape :

  1. reçoit la requête et la réponse;
  2. accomplit une responsabilité particulière;
  3. décide ensuite de poursuivre, de répondre ou de signaler une erreur.

Cette organisation rappelle le patron architectural pipe-et-filtre.

Express n’implémente pas nécessairement une forme pure de ce patron : les middlewares partagent notamment les mêmes objets req et res et peuvent interrompre la chaîne.

Une bonne chaîne de middlewares est composée de petites étapes ayant chacune une responsabilité claire.

Cela permet de :

  • réutiliser une même étape sur plusieurs routes;
  • insérer ou retirer une vérification;
  • modifier l’ordre des traitements;
  • garder les gestionnaires de route centrés sur leur responsabilité principale.

L’authentification et l’autorisation illustrent particulièrement bien cette séparation des responsabilités.

Authentifier consiste à établir l’identité de l’utilisateur.

Autoriser consiste ensuite à déterminer si cet utilisateur possède les droits nécessaires.

Ce sont deux responsabilités différentes.

On peut donc créer deux middlewares :

authentifier
↓
exigerRole("admin")
↓
gestionnaire

Pour comprendre l’ordre nécessaire, on peut décrire simplement ce que chaque middleware reçoit et ce qu’il garantit.

Il recherche une preuve d’identité dans la requête.

S’il appelle next(), il garantit que :

req.utilisateur

est défini.

Sinon, il répond 401 Unauthorized.

Il suppose que :

req.utilisateur

a déjà été défini.

Il vérifie ensuite si l’utilisateur possède le rôle demandé.

S’il possède ce rôle, le traitement continue.

Sinon, le middleware répond 403 Forbidden.

L’autorisation dépend du résultat de l’authentification. Leur ordre dans la chaîne n’est donc pas interchangeable.
Scénarios :
Middlewares :
Client
1journ.
gestionnaire
GET /api/ressource
next()
200{ "rapports": [] }
Ajoutez authentification puis autorisation, et observez la séquence
VérificationOn place exigerRole('admin') avant authentifier. La requête contient pourtant un jeton valide appartenant à un administrateur. Quel est le problème ?

exigerRole s’exécute avant que l’identité ait été établie. req.utilisateur n’a donc pas encore été défini. L’utilisateur peut être administrateur, mais le middleware d’autorisation ne dispose pas encore de cette information.

Un middleware Express est une étape intermédiaire du traitement d’une requête.

Il peut notamment :

  • observer ou modifier req et res;
  • appeler next() pour poursuivre;
  • produire directement une réponse;
  • transmettre une erreur;
  • préparer des informations pour les étapes suivantes.

L’ordre dans lequel les middlewares sont montés détermine leur ordre d’exécution.