Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 418 minIntermédiaire

Middleware Express : next(), erreurs async et route 404

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • expliquer le chemin d'une requête à travers la chaîne des middlewares ;
  • écrire un middleware et le brancher au bon niveau ;
  • laisser Express 5 attraper les erreurs des fonctions async ;
  • renvoyer toutes les erreurs dans un format unique, avec une route 404.

4 exercices corrigés1 schéma

Un middleware Express est une fonction qui s'exécute entre l'arrivée de la requête et l'envoi de la réponse : elle reçoit req, res et next, peut lire ou compléter la requête, répondre elle-même, ou passer la main au suivant en appelant next(). Toute une application Express est une chaîne de middlewares : le journal, la lecture du JSON, l'authentification, la validation, puis la route. Ce chapitre explique cette chaîne, montre comment écrire tes propres middlewares et met en place la gestion centralisée des erreurs, avec ce qu'Express 5 a changé pour les fonctions async.

Le principe : une chaîne de fonctions

Quand une requête arrive, Express la fait passer dans les middlewares dans l'ordre où tu les as enregistrés. Chacun a trois possibilités :

  1. passer la main au suivant avec next() (après avoir éventuellement ajouté quelque chose à req) ;
  2. répondre avec res.json(…), res.sendStatus(…) : la chaîne s'arrête là ;
  3. signaler une erreur avec next(erreur) ou throw : Express saute directement au gestionnaire d'erreurs.
niveau application : app.use(…)RequêtePOST …journalpino-httphelmeten-têtescorsoriginesexpress.jsonlit req.bodyrouteur /v1/fiches, puis la route : routeurFiches.post(…)authentifierjetonexigerRolerôlevaliderZodcontrôleurservice, dépôt201 Createdres.json(…)chaque étape appelle next()401403422404, 409routeInconnueaucune route : 404gestionnaireErreurs(err, req, res, next)une seule réponse d'erreur, au format application/problem+json
Chaque middleware passe la main avec next(), répond, ou envoie l'erreur au gestionnaire placé en dernier

Une route est d'ailleurs un middleware comme un autre : un middleware qui répond toujours.

Écrire un middleware

Un middleware qui écrit dans la console la méthode, l'adresse, le code de statut et la durée de chaque requête :

js
function journal(req, res, next) {
  const debut = performance.now();
  res.on('finish', () => {
    const duree = (performance.now() - debut).toFixed(1);
    console.log(`${req.method} ${req.originalUrl} → ${res.statusCode} (${duree} ms)`);
  });
  next();
}

app.use(journal);
text
GET /v1/fiches/12 → 500 (2.6 ms)
POST /v1/fiches → 201 (5.3 ms)
GET /v1/frais → 404 (0.2 ms)

Le middleware ne connaît pas encore le code de statut au moment où il s'exécute : la route n'a pas répondu. Il s'abonne donc à l'événement finish de la réponse, déclenché quand elle est envoyée, et appelle tout de suite next(). Le chapitre 11 remplace ce journal maison par pino.

Un deuxième exemple, qui refuse une requête : l'API n'accepte que du JSON pour les créations et les modifications.

js
function exigerJson(req, res, next) {
  if (['POST', 'PUT', 'PATCH'].includes(req.method) && !req.is('application/json')) {
    return res.status(415).json({ erreur: 'Envoie le corps en JSON (Content-Type: application/json).' });
  }
  next();
}

req.is(type) compare le Content-Type de la requête. 415 Unsupported Media Type est le code prévu pour un format de corps refusé. Une requête refusée ne va jamais plus loin : la route ne s'exécute pas.

L'ordre d'enregistrement compte

Express exécute les middlewares dans l'ordre des app.use et des routes. Les conséquences pratiques :

  • express.json() doit être enregistré avant les routes qui lisent req.body ;
  • le journal se place en premier, pour mesurer et tracer toutes les requêtes, même celles qui seront refusées ;
  • l'authentification se place avant les routes qu'elle protège ;
  • le middleware 404 et le gestionnaire d'erreurs se placent en dernier, après toutes les routes.
js
// src/app.js : l'ordre de l'API AppliFrais
app.use(journal);            // 1. tracer toutes les requêtes
app.use(express.json());     // 2. lire les corps JSON

app.get('/sante', (req, res) => res.json({ statut: 'ok' }));
app.use('/v1/fiches', routeurFiches);   // 3. les routes

app.use(routeInconnue);      // 4. rien n'a répondu : 404
app.use(gestionnaireErreurs); // 5. une erreur est survenue quelque part

Les trois niveaux : application, routeur, route

Un middleware peut s'appliquer à des portées différentes :

js
// Niveau application : toutes les requêtes
app.use(express.json());

// Niveau application, limité à un préfixe : tout ce qui commence par /v1
app.use('/v1', exigerJson);

// Niveau routeur : toutes les routes de ce routeur
routeurFiches.use(authentifier);

// Niveau route : une seule route, une liste de middlewares avant le contrôleur
routeurFiches.post('/:id/validations', exigerRole('comptable'), valider({ body: schemaValidation }), fiches.valider);

Le dernier niveau est très lisible : la ligne de route dit qui peut l'appeler et ce qui est vérifié avant d'atteindre le contrôleur. C'est la forme utilisée dans tout le reste du cours.

exigerRole('comptable') et valider({ … }) ne sont pas des middlewares mais des fabriques : des fonctions qui renvoient un middleware configuré. C'est le moyen de paramétrer un middleware :

js
// Une fabrique : exigerRole('comptable') renvoie un middleware qui connaît le rôle attendu
export function exigerRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.utilisateur?.role)) {
      throw new ErreurApi(403, 'acces-refuse', 'Accès refusé', 'Ton rôle ne permet pas cette opération.');
    }
    next();
  };
}

Transmettre une information au suivant

Les middlewares communiquent en ajoutant des propriétés à req. Le middleware d'authentification du chapitre 9 vérifie le jeton et range l'utilisateur dans req.utilisateur ; les middlewares et contrôleurs suivants le lisent. Express propose aussi res.locals, un objet vide prévu pour ça, surtout utilisé par les moteurs de gabarits.

Choisis des noms qui ne risquent pas d'écraser une propriété d'Express : req.utilisateur, req.valide, plutôt que req.query (en lecture seule en Express 5) ou req.user (utilisé par d'autres bibliothèques comme Passport).

Les middlewares prêts à l'emploi

MiddlewareRôleChapitre
express.json()Lit un corps JSON (100 Ko au plus par défaut) et remplit req.body3
express.urlencoded()Lit un formulaire HTML classique3
express.static(dossier)Sert des fichiers statiques3
pino-httpÉcrit une ligne de journal structurée par requête11
helmetAjoute les en-têtes de sécurité HTTP10
corsAutorise un front hébergé sur une autre origine10
express-rate-limitLimite le nombre de requêtes par client9 et 10
multerReçoit des fichiers envoyés par formulaire8
swagger-ui-expressPublie la documentation interactive13

Les erreurs des fonctions async

Une route qui interroge la base est async. Que se passe-t-il si la base ne répond pas ?

js
app.get('/v1/fiches/:id', async (req, res) => {
  const fiche = await depot.trouverParId(Number(req.params.id)); // MySQL arrêté : la promesse est rejetée
  res.json(fiche);
});

Avec Express 5, une fonction async qui lève une erreur, ou une promesse rejetée renvoyée par un middleware, est transmise automatiquement au gestionnaire d'erreurs, exactement comme si tu avais appelé next(erreur). Le client reçoit une réponse 500 propre.

Avec Express 4, ce même code laissait la requête sans réponse et produisait une promesse rejetée non gérée. Il fallait entourer chaque route d'un try … catch qui appelait next(erreur), ou installer le paquet express-async-errors. Si tu lis ce genre de code dans un tutoriel, sache qu'il n'est plus nécessaire :

js
// Écriture Express 4 : inutile en Express 5
app.get('/v1/fiches/:id', async (req, res, next) => {
  try {
    res.json(await depot.trouverParId(Number(req.params.id)));
  } catch (erreur) {
    next(erreur);
  }
});

Une limite demeure : Express ne voit que ce qui se passe dans la fonction ou la promesse qu'elle renvoie. Une erreur levée plus tard dans un rappel (setTimeout, un événement, une ancienne API à rappels) lui échappe. Transforme ces API en promesses (la plupart des modules de Node.js en ont une version, comme node:fs/promises) et utilise await.

Des erreurs métier avec une classe ErreurApi

Dans une API, beaucoup d'« erreurs » sont prévues : la fiche n'existe pas (404), elle est déjà clôturée (409), les données sont invalides (422). Plutôt que d'écrire res.status(…).json(…) partout, on lève une erreur qui connaît son code, et un seul endroit la transforme en réponse.

js
// src/erreurs.js
export class ErreurApi extends Error {
  constructor(status, type, titre, detail, extensions = {}) {
    super(detail);
    this.status = status;   // code HTTP
    this.type = type;       // identifiant court du type de problème
    this.titre = titre;     // résumé lisible, le même pour toutes les occurrences
    this.extensions = extensions; // informations en plus (champs en erreur…)
  }
}
js
// Dans un contrôleur ou un service
if (fiche.etat !== 'CR') {
  throw new ErreurApi(409, 'transition-interdite', "Changement d'état interdit",
    `Une fiche à l'état ${fiche.etat} ne peut pas passer à l'état ${nouvelEtat}.`, { etatActuel: fiche.etat });
}

throw interrompt la fonction, qu'elle soit synchrone ou async : Express transmet l'erreur au gestionnaire. Plus de return oublié, plus de double réponse.

La route 404 et le gestionnaire d'erreurs

Deux middlewares ferment la chaîne. Le premier attrape les requêtes qu'aucune route n'a traitées ; le second, reconnaissable à ses quatre paramètres, reçoit toutes les erreurs.

js
// src/erreurs.js (suite)
export function routeInconnue(req, res, next) {
  next(new ErreurApi(404, 'route-inconnue', 'Route inconnue',
    `Aucune route ne correspond à ${req.method} ${req.path}.`));
}

export function gestionnaireErreurs(err, req, res, next) {
  if (res.headersSent) return next(err); // la réponse est partie : on laisse Express couper la connexion

  let erreur = err;
  if (err.type === 'entity.parse.failed') {
    erreur = new ErreurApi(400, 'json-invalide', 'JSON invalide', "Le corps de la requête n'est pas du JSON valide.");
  } else if (err.type === 'entity.too.large') {
    erreur = new ErreurApi(413, 'corps-trop-gros', 'Requête trop volumineuse', 'Le corps de la requête dépasse la taille permise.');
  } else if (!(err instanceof ErreurApi)) {
    console.error(err); // le détail technique pour toi, jamais pour le client
    erreur = new ErreurApi(500, 'erreur-interne', 'Erreur interne', 'Une erreur inattendue est survenue.');
  }

  res.status(erreur.status).type('application/problem+json').json({
    type: `https://api.gsb.example/problemes/${erreur.type}`,
    title: erreur.titre,
    status: erreur.status,
    detail: erreur.message,
    instance: req.originalUrl,
    ...erreur.extensions,
  });
}

Toutes les erreurs de l'API ont maintenant la même forme, celle de la RFC 9457 (Problem Details for HTTP APIs) présentée dans le cours API REST. Le front n'a qu'une façon de les lire : status pour réagir, detail pour afficher, type pour distinguer les cas.

http
HTTP/1.1 409 Conflict
Content-Type: application/problem+json; charset=utf-8

{"type":"https://api.gsb.example/problemes/transition-interdite","title":"Changement d'état interdit","status":409,"detail":"Une fiche à l'état CR ne peut pas passer à l'état VA.","instance":"/v1/fiches/12","etatActuel":"CR"}

Trois cas méritent un traitement à part :

  • un JSON mal formé (une virgule en trop) : express.json() lève une erreur de type entity.parse.failed. C'est une faute du client : 400, pas 500 ;
  • un corps trop gros : entity.too.large, 413 ;
  • toute autre erreur, imprévue (MySQL arrêté, bogue) : 500 avec un message générique. La trace de la pile, le texte SQL ou le chemin d'un fichier ne sortent jamais vers le client : ils aident un attaquant à cartographier le serveur. Le détail va dans le journal ; le chapitre 11 ajoute un identifiant d'incident qui relie la réponse à sa ligne de journal.

Les erreurs en dehors des requêtes

Une promesse rejetée que personne n'attend (unhandled rejection) ou une exception levée hors de toute requête arrête le processus Node.js : c'est le comportement par défaut depuis Node.js 15, et c'est le bon. Un programme dans un état inconnu ne doit pas continuer à servir des requêtes. En production, c'est le gestionnaire de processus (Docker, systemd) qui relance l'API (chapitre 14). Ton travail : écrire l'erreur dans le journal avant l'arrêt, et ne pas laisser de promesse sans await.

Exercices

Exercice 1 sur 4 Suivre la chaîne Réussi À revoir

js
app.use((req, res, next) => { console.log('A'); next(); });
app.get('/sante', (req, res) => { console.log('B'); res.json({ statut: 'ok' }); });
app.use((req, res, next) => { console.log('C'); next(); });
app.use((req, res) => { console.log('D'); res.status(404).json({ erreur: 'Route inconnue' }); });

Qu'affiche la console, et quel code reçoit le client, pour GET /sante, puis pour GET /v1/fiches ?

Voir la réponseMasquer la réponse de l'exercice 1
  • GET /sante : A, B. Le client reçoit 200. La route répond et n'appelle pas next() : C et D ne s'exécutent pas.
  • GET /v1/fiches : A, C, D. Aucune route ne correspond ; la requête traverse les middlewares jusqu'au dernier, qui répond 404.

Tu avais trouvé ?

Exercice 2 sur 4 La requête qui ne répond jamais Réussi À revoir

Dans Bruno, GET /v1/fiches tourne pendant trente secondes puis échoue sans réponse. Voici le seul middleware ajouté depuis la dernière version qui marchait. Trouve le problème et corrige-le.

js
app.use((req, res, next) => {
  if (req.get('X-Maintenance') === 'oui') {
    res.status(503).json({ erreur: 'Maintenance en cours' });
  }
});
Voir la réponseMasquer la réponse de l'exercice 2

Quand l'en-tête X-Maintenance est absent, le middleware ne répond pas et n'appelle pas next() : la requête reste en suspens jusqu'au délai du client.

js
app.use((req, res, next) => {
  if (req.get('X-Maintenance') === 'oui') {
    return res.status(503).json({ erreur: 'Maintenance en cours' });
  }
  next();
});

Le return évite d'appeler next() après avoir répondu.

Tu avais trouvé ?

Exercice 3 sur 4 Écrire une fabrique de middleware Réussi À revoir

Écris une fabrique limiterTaille(max) qui refuse avec 413 et une ErreurApi toute requête dont l'en-tête Content-Length dépasse max octets. Montre comment l'appliquer aux seules routes du routeur des frais, avec 10 000 octets.

Voir la réponseMasquer la réponse de l'exercice 3
js
export function limiterTaille(max) {
  return (req, res, next) => {
    const taille = Number(req.get('Content-Length') ?? 0);
    if (taille > max) {
      throw new ErreurApi(413, 'corps-trop-gros', 'Requête trop volumineuse',
        `Le corps dépasse ${max} octets.`);
    }
    next();
  };
}

routeurFrais.use(limiterTaille(10_000));

La fabrique renvoie un middleware qui « se souvient » de max (une fermeture). En pratique, express.json({ limit: '10kb' }) fait déjà ce contrôle pour les corps JSON, y compris quand l'en-tête Content-Length est absent.

Tu avais trouvé ?

Exercice 4 sur 4 Quelle réponse ? Réussi À revoir

L'API utilise le gestionnaireErreurs de ce chapitre. Pour chaque situation, donne le code de statut et le champ type de la réponse.

  1. Le client envoie {"mois": "2026-11",} avec Content-Type: application/json.
  2. Un service lève new ErreurApi(404, 'fiche-introuvable', 'Fiche introuvable', 'La fiche 99 n\'existe pas.').
  3. MySQL est arrêté pendant GET /v1/fiches/12.
  4. Le client demande DELETE /v1/frais/3, qui n'existe pas.
  5. Le gestionnaire a été écrit function gestionnaireErreurs(err, req, res) { … } et le service lève l'erreur du point 2.
Voir la réponseMasquer la réponse de l'exercice 4
  1. 400, type …/json-invalide : la virgule finale rend le JSON invalide (entity.parse.failed).
  2. 404, type …/fiche-introuvable.
  3. 500, type …/erreur-interne, avec un message générique ; le détail (ECONNREFUSED) part dans la console ou le journal.
  4. 404, type …/route-inconnue, produit par routeInconnue.
  5. Avec trois paramètres, la fonction n'est pas un gestionnaire d'erreurs : Express l'ignore pour les erreurs et utilise son gestionnaire par défaut, qui répond 404 (il reprend le status de l'erreur) avec une page HTML au lieu du JSON prévu. Pire : hors production (NODE_ENV différent de production), cette page affiche la pile d'appels, avec les chemins des fichiers du serveur. Il faut les quatre paramètres.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Créer une API REST CRUD avec Node.js et ExpressRevoir le sommaire du cours