Apprendre Express.js : Middlewares

Les middlewares sont des fonctions centrales au framework Express, s'exécutant successivement dans le cycle requête-réponse HTTP.

Icône de calendrier
Intermédiaire
8 chapitres

Qu’est-ce qu’un middleware ?

Dans une application Node.js qui utilise Express, les fonctions middleware sont des fonctions intermédiaires qui sont appelées successivement pour traiter une requête HTTP. Elles peuvent effectuer une variété d’actions, comme :

  • Valider des données transmises via la requête
  • Accéder à une base de données
  • Jouer le rôle de pare-feu
  • Loguer des informations (journalisation)
  • Compresser une réponse
  • Gérer des erreurs
  • Mettre en cache une réponse
  • Construire la réponse à retourner au client
  • Etc.

Les middlewares sont des fonctions qui peuvent accéder à l’objet Request (req) et Response (res) ainsi qu’à une fonction middleware suivante dans le cycle requête-réponse de l’application (généralement désignée par une variable nommée next).

Si la fonction middleware en cours ne termine pas le cycle de requête-réponse, elle doit appeler la fonction next() pour transmettre le contrôle à la fonction middleware suivant. Sinon, la demande restera bloquée.

Écrire un middleware

Un middleware est donc une fonction définie avec les arguments, req, res et next (optionnel s’il est dernier dans le cycle de requête-réponse).

Voici un exemple d’une fonction middleware qui imprime en console la date actuelle, la méthode HTTP et le chemin des requêtes HTTP envoyées au serveur :

copié !
const logger = function(req, res, next) {
	const { method, url } = req;
	const date = new Date().toLocaleString();
	console.log(`${date}: ${method} ${url}`);
	next();
}
  • function : fonction anonyme de middleware
  • req : argument de requête HTTP à la fonction middleware, appelé req par convention
  • res : argument de réponse HTTP à la fonction middleware, appelé res par convention
  • next : argument de rappel à la fonction middleware, appelée next par convention (détaillée en fin de chapitre)

Utiliser un middleware

Pour charger une fonction middleware, on appelle app.use(). On dit que cette méthode « monte » un middleware sur un chemin donné.

copié !
app.use([path,] callback)
  • path (optionnel) : le chemin sur lequel le middleware doit être monté. Si le chemin n’est pas spécifié, le middleware prendra la valeur / et sera ainsi monté sur toutes les requêtes.
  • callback : la fonction middleware qui sera exécutée pour chaque requête reçue. La fonction reçoit les objets req (requête), res (réponse) et next (prochain middleware) en arguments.

Par exemple, à chaque fois que l’application reçoit une demande, elle charge la fonction middleware logger. Comme seule une fonction de callback est précisée via app.use(), ce middleware s’exécutera pour l’ensemble des routes.

app.js
copié !
const express = require('express');
const app = express();

const logger = function(req, res, next) {
	const { method, url } = req;
	const date = new Date().toLocaleString();
	console.log(`${date}: ${method} ${url}`);
	next();
}

app.use(logger);

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;

app.listen(port, hostname, () => {
	console.log(`Serveur démarré sur http://${hostname}:${port}`);
});

Piles de middlewares

Vous l’aurez compris lorsque nous avons abordé le fameux argument facultatif next, il est bien entendu possible pour des middlewares de se succéder, on parle de pile d’exécution.

Enchaînement ordonné

Si plusieurs middlewares matchent avec une même route, alors ils s’exécuteront dans l’ordre défini par le code (de haut en bas).

Par exemple, à chaque fois que l’application reçoit une demande, elle charge la fonction middleware logger qui log la requête HTTP, puis traite la demande à la fonction middleware suivante (dans l’exemple ci-dessous : la route pour le chemin racine) dans la pile en appelant la fonction next().

app.js
copié !
const express = require('express');
const app = express();

const logger = function(req, res, next) {
	const { method, url } = req;
	const date = new Date().toLocaleString();
	console.log(`${date}: ${method} ${url}`);
	next();
}

app.use(logger);

app.get('/', function (req, res) {
	res.send('Hello World!');
});

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;

app.listen(port, hostname, () => {
	console.log(`Serveur démarré sur http://${hostname}:${port}`);
});

Voici un exemple de pile de middlewares dans Express :

app.js
copié !
const express = require('express');
const app = express();

app.use((req, res, next) => {
	console.log('Middleware 1');
	next();
});

app.use((req, res, next) => {
	console.log('Middleware 2');
	next();
});

app.get('/', (req, res) => {
	res.send('Hello World!');
});

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;

app.listen(port, hostname, () => {
	console.log(`Serveur démarré sur http://${hostname}:${port}`);
});

Dans cet exemple, nous avons défini deux middlewares qui seront exécutés pour chaque requête reçue par l’application. Les deux middlewares affichent un message dans la console et appellent next() pour passer la main au middleware suivant dans la pile.

Ensuite, nous avons défini une route pour la méthode HTTP GET sur la route /. Cette route renvoie simplement une réponse “Hello World!” au client.

Lorsqu’un client envoie une requête GET vers la route /, l’ordre d’exécution des middlewares sera le suivant :

  1. Middleware 1 : affiche “Middleware 1” dans la console et appelle next().
  2. Middleware 2 : affiche “Middleware 2” dans la console et appelle next().
  3. Route handler : renvoie la réponse “Hello World” au client.

La pile de middlewares est donc Middleware 1 -> Middleware 2 -> Route /.

Quelques remarques :

  • Si next() n’avait pas été appelé par Middleware 1, alors aucune réponse ne serait retournée car l’application resterait bloquée sur ce middleware.
  • Si Route / avait été déclarée avant Middleware 1 et Middleware 2, alors ils n’auraient jamais été exécutés (absence de next()).
  • Si Middleware 1 avait terminé le cycle de réponse avec res.send() par exemple, alors une erreur sera provoquée par le res.send de la Route /, car la réponse aura déjà été retournée au client.

Notez que l’ordre d’exécution des middlewares est important : si nous avions défini la route avant les middlewares, elle aurait été exécutée avant les middlewares, même si ceux-ci étaient montés sur l’application.

Enchaînement via plusieurs fonctions de rappels

Vous pouvez d’ailleurs aussi définir en arguments plusieurs fonctions de rappel qui vont se comporter comme des middlewares. Ces dernières peuvent être déclarées :

  • Dans des variables et transmises via un tableau
  • Directement au sein d’un middleware
copié !
const m1 = function (req, res, next) {
	console.log('Je suis n°1');
	next();
}

const m2 = function (req, res, next) {
	console.log('Je suis n°2');
	next();
}

app.get('/path', [m1, m2], function (req, res, next) {
	console.log('Je suis n°3');
	next();
}, function (req, res) {
	res.send("Je suis la réponse ! J'interviens en dernier.");
});

Ici les middlewares sont montés sur /path et s’exécutent dans l’ordre d’écriture :

> console.log('Je suis n°1');
> console.log('Je suis n°2');
> console.log('Je suis n°3');
> res.send("Je suis la réponse ! J'interviens en dernier.");
Ignorer les fonctions middleware issues d'une pile de middleware de route

Pour ignorer les fonctions middleware issues d’une pile de middleware de routeur et passer le contrôle à la prochaine route, il faut appeler next('route').

copié !
app.get('/user/:id', function (req, res, next) {
	if (req.params.id == 0) next('route');
	else next();
}, function (req, res) {
	res.send('Scénario normal');
});

app.get('/user/:id', function (req, res) {
	res.send('Scénario anormal');
});

Middlewares dans Express

Une application Express est une infrastructure web minimaliste qui n’est ni plus ni moins qu’une succession d’appels de fonctions middleware.

Il existe 5 types de middlewares dans Express : les middlewares d’application, les middlewares de route, les middlewares d’erreur, les middlewares intégrés et les middlewares tiers.

Middleware d’application

Un middleware d’application est lié à une instance de l’objet app d’Express par les fonctions app.use() et app.METHOD() (où METHOD est la méthode HTTP de la demande que gère la fonction middleware - get, post…).

Middleware sans chemin de montage

Il peut être déclaré sans chemin de montage, alors la fonction sera exécutée à chaque fois que l’application reçoit une demande :

copié !
app.use(function (req, res, next) {
	res.send('Hello World!');
});

Middleware avec chemin de montage

Il peut aussi être déclaré avec un chemin de montage, sans préciser de méthode HTTP avec app.use() :

copié !
app.use('/users/:id', function (req, res, next) {
	res.send('Hello World!');
});

Il peut aussi être déclaré avec un chemin de montage, en précisant la méthode HTTP avec app.METHOD() :

copié !
app.get('/users/:id', function (req, res, next) {
	res.send('Hello World!');
});

Middleware de route

Le middleware de route fonctionne de la même manière que le middleware d’application, à l’exception près qu’il est lié à une instance d’express.Router() et non d’express().

copié !
const router = express.Router();

En voici un exemple :

app.js
copié !
const express = require('express');
const app = express();
const router = express.Router();

// Une fonction middleware sans chemin qui s'exécute pour chaque requête vers le routeur
router.use(function (req, res, next) {
	console.log('Hello World!');
	next();
});

router.get('/foo', (req, res) => {
	res.send("Foo");
});

router.get('/bar', (req, res) => {
	res.send("Bar");
});

// On monte le routeur sur l'app
app.use('/demo', router);

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;

app.listen(port, hostname, () => {
	console.log(`Serveur démarré sur http://${hostname}:${port}`);
});

L’utilisation d’un middleware de route permet de définir des middlewares pour un ensemble de routes spécifique. Cela contribue à rendre votre code plus lisible et plus facile à maintenir que de passer systématiquement par des middlewares d’application avec chemin de montage.

Middleware de traitement d’erreur

Express nous propose un middleware un peu spécial dédié à la gestion des erreurs. À la différence d’un middleware standard il va être constitué de 4 arguments, au lieu de 3, dont le premier sera err, contenant des informations sur une éventuelle erreur :

copié !
app.use(function(err, req, res, next) {
	console.error(err.stack);
	res.status(500).send('Oups');
});

Les réponses peuvent être au format de votre choix :

  • Une page d’erreur HTML
  • Un message en console
  • Une chaîne JSON…

Vous retrouverez de plus amples informations sur la gestion des erreurs avec Express sur la documentation officielle.

Middleware intégré

Les middlewares intégrés sont des middlewares directement intégrés à Express dans le but d’effectuer des tâches courantes telles que :

  • La gestion des corps de requête
  • La gestion des sessions
  • La gestion des cookies
  • La sécurité
  • La gestion des assets
  • Etc.

À l’origine nombreux, ils ont, depuis la version 4.X d’Express, été remplacés par des modules tiers, à l’exception de express.json(), express.urlencoded() et express.static().

Middleware tiers

Si vous souhaitez ajouter d’autres fonctionnalités à Express, alors il faudra compter sur des modules tiers, développés par la communauté Node.

Ces middlewares peuvent classiquement être installés via votre package manager favori tel que npm et être utilisés pour des tâches en tout genre. Exemple : cors, mongoose, morgan

Retrouvez sur la documentation officielle les principaux modules utilisés avec Express.