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
/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.
| Couche | Rôle | Exemple dans Express |
|---|---|---|
| Model | Données & logique métier | models/article.js |
| View | Présentation (JSON pour API) | Réponse res.json() |
| Controller | Traitement, intermédiaire | controllers/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;
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;
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');
});
/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.