Retour au cours

backend / nodejs

Routing avancé

Leçon 81 exercice

Explication

Ce que vous allez apprendre

  • Centraliser la validation d'un paramètre partagé avec router.param
  • Composer des routers imbriqués pour structurer une API qui grossit (/api/v1/...)
  • Propager les params d'un router parent vers un router enfant avec mergeParams
  • Répondre dans plusieurs formats selon l'en-tête Accept avec res.format()
  • Appliquer un rate limiting ciblé sur une route sensible précise

Dans quel contexte ?

Une API de gestion de commandes structure ses routes en /users/:userId/orders, où chaque route doit d'abord vérifier que l'utilisateur userId existe avant de faire quoi que ce soit d'autre. Sans factorisation, cette vérification est copiée-collée au début de chaque route qui utilise ce paramètre. Un développeur imbrique aussi un router orders.js sous /users/:userId/orders et découvre que req.params.userId vaut undefined dans ce router enfant — un piège précis que cette leçon explique et corrige.

Quand les routes simples ne suffisent plus

Les leçons précédentes ont montré des routes isolées, indépendantes les unes des autres. Sur une vraie API avec des dizaines de ressources liées entre elles (utilisateurs, leurs commandes, leurs commentaires), revalider ou recharger les mêmes données dans chaque route devient vite répétitif.

Voyons d'abord un premier outil pour factoriser ça : router.param. Plutôt que de revérifier "cet utilisateur existe-t-il ?" au début de CHAQUE route qui utilise :userId, cette fonction centralise la vérification en un seul endroit.

Concrètement, comment ça fonctionne ? Elle s'exécute automatiquement dès qu'une route contient ce paramètre, et injecte le résultat (req.user) pour que toutes les routes suivantes puissent l'utiliser directement, sans redemander à la base de données.

Une fois cette validation centralisée, une autre question se pose : comment organiser une API qui grossit ? Imbriquer des routers, comme /api/v1/users et /api/v1/products, permet d'organiser le code par domaine métier tout en gardant une hiérarchie d'URL cohérente.

Il faut connaître un détail technique important pour cette imbrication : mergeParams: true. Sans cette option, un router monté sous /users/:userId/orders ne verrait PAS userId dans ses propres routes.

C'est un piège fréquent la première fois qu'on imbrique des routers. Le paramètre semble avoir disparu, sans message d'erreur explicite pour orienter le débogage.

OutilRôle
router.paramCentralise la résolution/validation d'un paramètre partagé
Router({ mergeParams: true })Propage les params du router parent au router enfant
res.format()Répond dans un format différent selon l'en-tête Accept

Prérequis

Cette leçon suppose que tu es à l'aise avec Router() et les routes paramétrées (:id) vues dans la leçon sur les bases d'Express.

Une fois cette imbrication maîtrisée, un dernier concept mérite d'être connu : la négociation de contenu. res.format() permet à un même endpoint de répondre dans plusieurs formats (JSON, CSV, HTML) selon ce que le client demande via l'en-tête Accept.

C'est plus élégant que de créer des routes séparées comme /reports/:id.csv et /reports/:id.json. Le client choisit le format, le serveur s'adapte.

Le piège à connaître avant de pratiquer : oublier mergeParams: true sur un router imbriqué provoque une erreur silencieuse, req.params.userId vaut undefined dans le router enfant, ce qui casse toute logique qui en dépend.

Piège fréquent

Créer un router imbriqué avec Router() au lieu de Router({ mergeParams: true }) fait perdre silencieusement req.params.userId dans toutes les routes de ce router enfant, sans qu'Express ne signale d'erreur explicite — juste un undefined inattendu à l'exécution.

Commandes & code

Routing avancé

js
// Paramètres multiples et contraintes via regex
app.get("/products/:category/:id(\\d+)", (req, res) => {
  // :id(\\d+) n'accepte QUE des chiffres, ex. /products/tech/42
  res.json({ category: req.params.category, id: Number(req.params.id) });
});
js
// router.param — logique partagée à TOUTES les routes utilisant ce paramètre
router.param("userId", async (req, res, next, id) => {
  const user = await db.users.findById(id);
  if (!user) {
    return res.status(404).json({ error: "Utilisateur introuvable" });
  }
  req.user = user; // injecté pour toutes les routes suivantes
  next();
});

router.get("/users/:userId", (req, res) => res.json(req.user));
router.get("/users/:userId/orders", async (req, res) => {
  const orders = await db.orders.findByUserId(req.user.id);
  res.json(orders);
});
js
// Composition de routers imbriqués — API versionnée et modulaire
// routes/v1/index.js
import { Router } from "express";
import usersRouter from "./users.js";
import productsRouter from "./products.js";

const v1Router = Router();
v1Router.use("/users", usersRouter);
v1Router.use("/products", productsRouter);

export default v1Router;

// app.js
import v1Router from "./routes/v1/index.js";
app.use("/api/v1", v1Router);
// GET /api/v1/users/42
js
// mergeParams — accéder aux params du router PARENT dans un router imbriqué
// routes/orders.js — monté sous /users/:userId/orders
const ordersRouter = Router({ mergeParams: true });

ordersRouter.get("/", (req, res) => {
  // req.params.userId disponible ici grâce à mergeParams
  res.json({ userId: req.params.userId, orders: [] });
});

export default ordersRouter;

// app.js
app.use("/users/:userId/orders", ordersRouter);
js
// Content negotiation — répondre différemment selon l'en-tête Accept
app.get("/reports/:id", async (req, res) => {
  const report = await getReport(req.params.id);

  res.format({
    "application/json": () => res.json(report),
    "text/csv": () => {
      res.type("csv").send(reportToCsv(report));
    },
    default: () => res.status(406).json({ error: "Format non supporté" }),
  });
});
js
// Rate limiting par route spécifique (voir aussi la leçon sécurité pour un middleware global)
import rateLimit from "express-rate-limit";

const strictLimiter = rateLimit({ windowMs: 60_000, max: 5 });

app.post("/auth/login", strictLimiter, async (req, res) => {
  // maximum 5 tentatives de connexion par minute et par IP
});

Résumé

  • router.param centralise la résolution/validation d'un paramètre partagé par plusieurs routes.
  • mergeParams: true propage les params du router parent dans un router imbriqué monté sous un préfixe paramétré.
  • res.format() implémente proprement la négociation de contenu (JSON, CSV, HTML...).
  • Des routers composés en arborescence (/api/v1/users, /api/v1/products) structurent une API qui grossit.

Exercices pratiques

1 disponible
1

Mission : un userId qui disparaît dans un router imbriqué

Objectif : Diagnostiquer la perte silencieuse d'un paramètre parent dans un router imbriqué, puis factoriser une validation répétée avec router.param.

Contexte

L'API structure ses routes en /users/:userId/orders, avec un router ordersRouter séparé monté sur ce préfixe. Sur GET /users/42/orders, la réponse renvoie {"userId": undefined, "orders": [...]}" au lieu de {"userId": "42", ...}. Par ailleurs, six routes différentes commencent chacune par recharger l'utilisateur en base via db.users.findById(req.params.userId) avant de vérifier s'il existe, un code identique répété à chaque fois.

Résoudre l’exercice →