Retour au cours

backend / nodejs

Middlewares Express

Leçon 71 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le rôle de next() et le danger de l'oublier
  • Écrire un middleware conditionnel monté uniquement sur un préfixe de route
  • Créer un middleware paramétrable avec le pattern "factory"
  • Écrire un gestionnaire d'erreurs Express reconnu par sa signature à 4 paramètres
  • Propager correctement une erreur survenue dans un handler async

Dans quel contexte ?

Une route DELETE /products/:id doit être accessible uniquement aux utilisateurs ayant le rôle admin. Un développeur ajoute la vérification directement dans le handler de cette route, puis doit la recopier identique dans cinq autres routes sensibles. Cette leçon montre comment factoriser cette logique dans un middleware réutilisable et paramétrable, et comment gérer proprement le cas où une vérification échoue au milieu d'une chaîne de traitement.

Une image pour commencer

Imagine une chaîne de contrôles à l'aéroport : contrôle de sécurité, puis contrôle des passeports, puis embarquement. Chaque étape peut laisser passer le voyageur vers la suivante, ou l'arrêter net.

Un middleware Express joue exactement ce rôle. C'est une fonction qui s'insère dans le trajet d'une requête AVANT qu'elle n'atteigne sa destination finale.

Le mécanisme qui fait avancer la chaîne s'appelle next(). Appelé, il transmet la requête à l'étape suivante ; non appelé, sans envoyer de réponse, la requête reste bloquée indéfiniment — un bug très courant chez les débutants.

Une fois ce mécanisme compris, un détail compte énormément : l'ordre de déclaration. Les middlewares s'exécutent dans l'ordre EXACT où ils sont déclarés avec app.use().

Ce n'est pas un détail cosmétique. Un middleware d'authentification déclaré APRÈS une route sensible ne protégera jamais cette route, puisqu'Express l'aura déjà traitée — c'est pourquoi la sécurité et le logging sont généralement montés en tout premier.

MiddlewareParamètresOù le déclarer
Middleware normal(req, res, next)Avant les routes qui en dépendent
Middleware d'erreur(err, req, res, next) — exactement 4Toujours en dernier, après toutes les routes

Prérequis

Cette leçon suppose que tu es à l'aise avec les bases d'Express (routes, req/res) vues à la leçon précédente : un middleware s'insère dans ce même mécanisme de traitement des requêtes.

Il existe un cas spécial de middleware à connaître : le gestionnaire d'erreurs. Express le reconnaît à un détail précis, il doit avoir EXACTEMENT 4 paramètres (err, req, res, next), même si next n'est pas utilisé dans le corps.

Ce middleware particulier doit toujours être déclaré en DERNIER, après toutes les routes. Express ne l'appelle que si une erreur est explicitement transmise via next(err).

Il reste un piège classique et sournois à connaître, spécifique à Express 4 : les erreurs asynchrones. Une erreur levée dans une fonction async n'est PAS automatiquement transmise au gestionnaire d'erreurs.

Sans try/catch explicite et next(err), l'erreur reste silencieuse et le client ne reçoit jamais de réponse. Le pattern asyncHandler résout ce problème une fois pour toutes, en évitant de répéter un try/catch dans chaque route — et bonne nouvelle, Express 5 corrige ce comportement nativement.

Piège fréquent

Sous Express 4, une exception levée dans un handler async sans try/catch ni next(err) ne déclenche jamais le middleware d'erreur : la requête reste sans réponse jusqu'au timeout du client. Le wrapper asyncHandler (ou une migration vers Express 5) élimine ce piège une fois pour toutes.

Et la suite ? Cette leçon prolonge directement la précédente sur les bases d'Express, en creusant le mécanisme qui rend le framework aussi flexible.

Commandes & code

Middlewares Express

Un middleware est une fonction (req, res, next) exécutée dans l'ordre de déclaration.

js
// Middleware de logging global
function requestLogger(req, res, next) {
  const start = Date.now();

  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.originalUrl} -> ${res.statusCode} (${duration}ms)`);
  });

  next(); // OBLIGATOIRE : sans next(), la requête reste bloquée indéfiniment
}

app.use(requestLogger); // appliqué à TOUTES les routes
js
// Middleware conditionnel, monté uniquement sur un préfixe
function requireApiKey(req, res, next) {
  const apiKey = req.headers["x-api-key"];

  if (apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: "Clé API invalide" });
  }

  next();
}

app.use("/admin", requireApiKey); // seules les routes /admin/* passent par ce middleware
js
// Middleware avec configuration (factory pattern)
function requireRole(role) {
  return (req, res, next) => {
    if (req.user?.role !== role) {
      return res.status(403).json({ error: "Permission refusée" });
    }
    next();
  };
}

app.delete("/products/:id", requireRole("admin"), (req, res) => {
  res.status(204).send();
});
js
// Middleware d'erreur — 4 arguments obligatoires (req, res, next ne suffisent pas)
// Express le reconnaît UNIQUEMENT si la fonction a exactement 4 paramètres
function errorHandler(err, req, res, next) {
  console.error(err.stack);

  const status = err.status ?? 500;
  res.status(status).json({
    error: status === 500 ? "Erreur interne" : err.message,
  });
}

// TOUJOURS déclaré en DERNIER, après toutes les routes
app.use(errorHandler);
js
// Propager une erreur async vers le error handler — piège classique Express 4
app.get("/risky", async (req, res, next) => {
  try {
    const data = await fetchSomethingThatMayFail();
    res.json(data);
  } catch (err) {
    next(err); // sans ça, une erreur async non catchée crash le process en Express < 5
  }
});

// Express 5+ : les erreurs de handlers async sont automatiquement transmises à next()
js
// Wrapper générique pour éviter de répéter try/catch partout
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get(
  "/orders/:id",
  asyncHandler(async (req, res) => {
    const order = await getOrder(req.params.id);
    if (!order) {
      const err = new Error("Commande introuvable");
      err.status = 404;
      throw err;
    }
    res.json(order);
  })
);

Résumé

  • Chaque middleware DOIT appeler next() (ou envoyer une réponse) sous peine de bloquer la requête.
  • L'ordre de app.use() détermine l'ordre d'exécution : logging et sécurité avant les routes métier.
  • Le middleware d'erreur nécessite exactement 4 paramètres (err, req, res, next).
  • Wrapper les handlers async (asyncHandler) évite les erreurs silencieusement perdues en Express 4.

Exercices pratiques

1 disponible
1

Mission : une route qui plante le serveur sans jamais répondre au client

Objectif : Corriger une erreur async non propagée qui laisse le client sans réponse, puis factoriser un contrôle d'accès dupliqué dans un middleware réutilisable.

Contexte

La route GET /orders/:id appelle await getOrder(req.params.id) dans un handler async sans try/catch, sur une API Express 4. Quand la base de données est temporairement indisponible, getOrder rejette une Promise, et le client reste bloqué jusqu'au timeout, sans jamais recevoir de réponse ni d'erreur 500. Par ailleurs, la vérification if (req.user?.role !== 'admin') return res.status(403)... est copiée-collée identique au début de 6 routes sensibles différentes.

Résoudre l’exercice →