1. express.Router()

express.Router() crée un routeur isolé — un mini-application Express avec ses propres routes et middlewares. On le monte ensuite sur l'application principale avec un préfixe.

Un routeur expose les mêmes méthodes que app : router.get(), router.post(), router.put(), router.delete(), etc. La différence clé : un routeur ne peut pas écouter un port — il délègue ça à l'app parent.

Pourquoi utiliser express.Router() ?

  • Organisation — chaque ressource a son propre fichier
  • Réutilisabilité — un routeur peut être monté à plusieurs préfixes
  • Maintenabilité — les équipes travaillent en parallèle sur des fichiers séparés
  • Tests faciles — tester un routeur indépendamment de l'app

Créer et monter un routeur basique

// routes/articles.js — Créer un routeur isolé
const express = require('express');
const router  = express.Router();

// Toutes ces routes sont relatives au préfixe de montage
// Si montage avec /api/articles, alors GET /api/articles/
router.get('/', (req, res) => {
  res.json({ success: true, data: [] });
});

// GET /api/articles/:id
router.get('/:id', (req, res) => {
  res.json({ success: true, data: { id: req.params.id } });
});

// POST /api/articles
router.post('/', (req, res) => {
  res.status(201).json({ success: true, data: req.body });
});

module.exports = router;

// ─────────────────────────────────────────────
// server.js — Monter le routeur avec un préfixe
const express        = require('express');
const app            = express();
const articlesRouter = require('./routes/articles');

app.use(express.json());

// Monter le routeur : toutes les routes /api/articles/* seront gérées
app.use('/api/articles', articlesRouter);

app.listen(3000, () => console.log('Serveur sur http://localhost:3000'));
// Test : GET http://localhost:3000/api/articles
// Test : GET http://localhost:3000/api/articles/42
Le préfixe /api/articles n'apparaît PAS dans le routeur — il est défini lors du montage dans server.js. Le routeur ne voit que la partie après le préfixe.

2. Architecture MVC

L'architecture MVC (Model-View-Controller) sépare les responsabilités en 3 couches. Pour une API REST, la "View" correspond à la réponse JSON.

CoucheRôleExemple dans Express
ModelDonnées & logique métiermodels/article.js
ViewPrésentation (JSON pour API)Réponse res.json()
ControllerTraitement, intermédiairecontrollers/articleController.js

Structure de dossiers recommandée

mon-api/
├── server.js                    # Point d'entrée
├── routes/
│   ├── articles.js              # Définitions des routes
│   └── users.js
├── controllers/
│   ├── articleController.js     # Logique métier
│   └── userController.js
├── models/
│   └── article.js               # Données / accès DB
└── package.json

Séparer route et controller

// controllers/articleController.js — Logique métier isolée
let articles = [
  { id: 1, title: 'Node.js pour débutants', author: 'Alice' },
  { id: 2, title: 'Express en profondeur',  author: 'Bob'   }
];
let nextId = 3;

exports.getAll = (req, res) => {
  res.json({ success: true, data: articles, meta: { total: articles.length } });
};

exports.getOne = (req, res) => {
  const article = articles.find(a => a.id === +req.params.id);
  if (!article) return res.status(404).json({ success: false, error: 'Article non trouvé' });
  res.json({ success: true, data: article });
};

exports.create = (req, res) => {
  const { title, author } = req.body;
  if (!title || !author)
    return res.status(400).json({ success: false, error: 'title et author sont requis' });
  const article = { id: nextId++, title, author };
  articles.push(article);
  res.status(201).json({ success: true, data: article });
};

exports.update = (req, res) => {
  const idx = articles.findIndex(a => a.id === +req.params.id);
  if (idx === -1) return res.status(404).json({ success: false, error: 'Article non trouvé' });
  articles[idx] = { ...articles[idx], ...req.body, id: articles[idx].id };
  res.json({ success: true, data: articles[idx] });
};

exports.remove = (req, res) => {
  const idx = articles.findIndex(a => a.id === +req.params.id);
  if (idx === -1) return res.status(404).json({ success: false, error: 'Article non trouvé' });
  articles.splice(idx, 1);
  res.status(204).send();
};

// ─────────────────────────────────────────────
// routes/articles.js — Routes uniquement, sans logique
const express    = require('express');
const router     = express.Router();
const ctrl       = require('../controllers/articleController');

router.get('/',    ctrl.getAll);
router.get('/:id', ctrl.getOne);
router.post('/',   ctrl.create);
router.put('/:id', ctrl.update);
router.delete('/:id', ctrl.remove);

module.exports = router;
Bonne pratique : les routes ne contiennent que router.verbe(chemin, handler). Toute la logique va dans le controller. Cela rend les tests unitaires beaucoup plus simples.

3. router.route() — Route chaînée

router.route(chemin) retourne un objet de route sur lequel on peut chaîner plusieurs méthodes HTTP. Cela évite de répéter le même chemin plusieurs fois.

Sans vs avec chaînage

// ❌ Sans chaînage — le chemin '/articles' est répété
router.get('/articles',    ctrl.getAll);
router.post('/articles',   ctrl.create);
// Et pour /:id
router.get('/articles/:id',    ctrl.getOne);
router.put('/articles/:id',    ctrl.update);
router.delete('/articles/:id', ctrl.remove);

// ✅ Avec router.route() — beaucoup plus propre
router.route('/articles')
  .get(ctrl.getAll)
  .post(ctrl.create);

router.route('/articles/:id')
  .get(ctrl.getOne)
  .put(ctrl.update)
  .delete(ctrl.remove);

CRUD complet avec router.route()

// routes/products.js — CRUD complet avec routes chaînées
const express = require('express');
const router  = express.Router();

let products = [
  { id: 1, name: 'Laptop', price: 999, category: 'tech' },
  { id: 2, name: 'Mouse',  price: 29,  category: 'tech' },
  { id: 3, name: 'Book',   price: 15,  category: 'education' }
];
let nextId = 4;

// GET /products + POST /products
router.route('/')
  .get((req, res) => {
    res.json({ success: true, data: products });
  })
  .post((req, res) => {
    const { name, price, category } = req.body;
    if (!name || price === undefined)
      return res.status(400).json({ success: false, error: 'name et price requis' });
    const product = { id: nextId++, name, price, category: category || 'misc' };
    products.push(product);
    res.status(201).json({ success: true, data: product });
  });

// GET /products/:id + PUT /products/:id + DELETE /products/:id
router.route('/:id')
  .get((req, res) => {
    const p = products.find(p => p.id === +req.params.id);
    if (!p) return res.status(404).json({ success: false, error: 'Produit non trouvé' });
    res.json({ success: true, data: p });
  })
  .put((req, res) => {
    const idx = products.findIndex(p => p.id === +req.params.id);
    if (idx === -1) return res.status(404).json({ success: false, error: 'Produit non trouvé' });
    products[idx] = { ...products[idx], ...req.body, id: products[idx].id };
    res.json({ success: true, data: products[idx] });
  })
  .delete((req, res) => {
    const before = products.length;
    products = products.filter(p => p.id !== +req.params.id);
    if (products.length === before)
      return res.status(404).json({ success: false, error: 'Produit non trouvé' });
    res.status(204).send();
  });

module.exports = router;
router.route() est particulièrement utile pour les ressources REST classiques où la même URL supporte plusieurs méthodes HTTP.

4. router.param() et middleware router-level

router.param(name, callback) définit un middleware de paramètre : il s'exécute automatiquement chaque fois que le paramètre nommé est présent dans une route. Idéal pour valider ou charger une ressource par ID avant le handler principal.

router.param() — Middleware par paramètre

// Middleware de paramètre — s'exécute quand :id est présent
router.param('id', (req, res, next, id) => {
  // 4 arguments : req, res, next, et la VALEUR du paramètre
  console.log(`Paramètre id reçu : ${id}`);

  // Valider que l'ID est un nombre
  if (isNaN(id))
    return res.status(400).json({ success: false, error: 'ID invalide — doit être un nombre' });

  // Charger la ressource et l'attacher à req
  const article = articles.find(a => a.id === +id);
  if (!article)
    return res.status(404).json({ success: false, error: 'Article non trouvé' });

  req.article = article; // Disponible dans tous les handlers suivants
  next();                // Passer au handler suivant
});

// Maintenant tous ces handlers ont accès à req.article
router.get('/:id',    (req, res) => res.json({ success: true, data: req.article }));
router.put('/:id',    (req, res) => {
  Object.assign(req.article, req.body);
  res.json({ success: true, data: req.article });
});
router.delete('/:id', (req, res) => {
  articles = articles.filter(a => a.id !== req.article.id);
  res.status(204).send();
});

router.use() — Middleware local au routeur

// router.use() s'applique seulement aux routes de CE routeur
// Différence clé : app.use() = global, router.use() = local

const articlesRouter = express.Router();

// Middleware de log local (uniquement pour /api/articles)
articlesRouter.use((req, res, next) => {
  console.log(`[Articles] ${req.method} ${req.path} — ${new Date().toISOString()}`);
  next();
});

// Middleware d'authentification local
articlesRouter.use((req, res, next) => {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).json({ success: false, error: 'Token requis' });
  }
  // Vérification simplifiée
  if (token !== 'Bearer secret123')
    return res.status(403).json({ success: false, error: 'Token invalide' });
  next();
});

// Ces routes nécessitent maintenant un token Authorization
articlesRouter.get('/',    getAll);
articlesRouter.post('/',   create);
// etc.

module.exports = articlesRouter;
Ordre important : router.use(middleware) doit être appelé AVANT les routes qu'il doit affecter. Un middleware déclaré après une route ne s'appliquera pas à cette route.

5. Fichiers de routes séparés

Dans une application réelle, chaque ressource a son propre fichier de routes dans le dossier routes/. Le server.js se contente de les importer et de les monter avec app.use().

Application complète avec 2 routeurs séparés

// routes/users.js — Routeur utilisateurs
const express = require('express');
const router  = express.Router();

let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
  { id: 2, name: 'Bob',   email: 'bob@example.com',   role: 'user'  }
];
let nextId = 3;

router.route('/')
  .get((req, res) => {
    res.json({ success: true, data: users });
  })
  .post((req, res) => {
    const { name, email, role = 'user' } = req.body;
    if (!name || !email)
      return res.status(400).json({ success: false, error: 'name et email requis' });
    const user = { id: nextId++, name, email, role };
    users.push(user);
    res.status(201).json({ success: true, data: user });
  });

router.route('/:id')
  .get((req, res) => {
    const user = users.find(u => u.id === +req.params.id);
    if (!user) return res.status(404).json({ success: false, error: 'Utilisateur non trouvé' });
    res.json({ success: true, data: user });
  })
  .put((req, res) => {
    const idx = users.findIndex(u => u.id === +req.params.id);
    if (idx === -1) return res.status(404).json({ success: false, error: 'Utilisateur non trouvé' });
    users[idx] = { ...users[idx], ...req.body, id: users[idx].id };
    res.json({ success: true, data: users[idx] });
  })
  .delete((req, res) => {
    const before = users.length;
    users = users.filter(u => u.id !== +req.params.id);
    if (users.length === before)
      return res.status(404).json({ success: false, error: 'Utilisateur non trouvé' });
    res.status(204).send();
  });

module.exports = router;

// ─────────────────────────────────────────────
// server.js — Monter les 2 routeurs
const express      = require('express');
const app          = express();
const PORT         = 3000;
const articlesRouter = require('./routes/articles');
const usersRouter    = require('./routes/users');

app.use(express.json());
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  next();
});

// Préfixes versionnés — bonne pratique pour les APIs
app.use('/api/v1/articles', articlesRouter);
app.use('/api/v1/users',    usersRouter);

app.get('/', (req, res) => res.json({
  success: true,
  message: 'API v1',
  endpoints: ['/api/v1/articles', '/api/v1/users']
}));

app.use('*', (req, res) => res.status(404).json({ success: false, error: 'Route introuvable' }));

app.listen(PORT, () => {
  console.log('Serveur sur http://localhost:' + PORT);
  console.log('  GET  /api/v1/articles');
  console.log('  GET  /api/v1/users');
});
Convention recommandée : utiliser des préfixes versionnés (/api/v1/, /api/v2/) dès le début. Quand vous faites une refonte, vous pouvez déployer /api/v2/ sans casser les clients existants.
← Accueil Exercices →