Préparation aux entretiens Node.js et Express : 70 questions avec des réponses précises.
Une revue structurée de 70 questions d’entretien sur Node.js et Express, couvrant le cycle d’événements, les flux, les middleware, la sécurité et l’escalabilité, avec des corrections pour les réponses courantes.
Sources: Node.js & Express.js Interview Guide
Les entretiens pour les postes Node.js portent généralement sur des sujets similaires : la manière dont un environnement d’exécution à thread unique gère des milliers de connexions, ce que fait réellement la boucle d’événements, et comment Express transforme un serveur HTTP brut en une application. Les réponses mémorisées échouent face à des questions complémentaires, c’est pourquoi cette revue regroupe 70 questions courantes par thème et fournit pour chaque réponse une explication suffisamment détaillée pour que vous puissiez la défendre. Lorsque la réponse courante est obsolète ou imprecise, la correction est indiquée, car c’est souvent la capacité à repérer ces détails qui distingue un candidat compétent d’un candidat simplement préparé.
Fondamentaux de Node.js
Q1. Qu’est-ce que Node.js ?
C’est un environnement d’exécution qui met en œuvre JavaScript en dehors du navigateur. Il intègre le moteur V8 de Google et ajoute des API pour les fichiers, les réseaux et les processus. Son modèle est basé sur des événements avec des entrées/sorties non bloquantes, ce qui le rend léger et particulièrement adapté aux services à forte charge de données en temps réel.
Q2. Node.js est-il à thread unique ?
Votre JavaScript s’exécute sur un seul thread principal, celui qui pilote la boucle d’événements. En arrière-plan, libuv gère un pool de threads pour les tâches qui ne disposent pas d’interface asynchrone fiable au niveau du système d’exploitation, comme les appels au système de fichiers, dns.lookup, certaines fonctions crypto et zlib. Ainsi, la réponse exacte est : à thread unique pour votre code, mais multi-threadé au niveau du système.
Q3. Pourquoi Node.js est-il rapide ?
- V8 compile JavaScript en code machine au lieu de l’interpréter.
- Les entrées et sorties sont asynchrones et non bloquantes.
Q4. Code synchrone versus asynchrone
Une opération synchrone se termine avant que la ligne suivante ne s’exécute, bloquant tout ce qui suit. Une opération asynchrone, comme la lecture d’un fichier ou une requête de base de données, est lancée puis transférée ; son exécution se poursuit et un callback ou une promesse fournit le résultat plus tard. Dans la version synchrone ci-dessous, le journal d’erreurs attend la fin de toute la lecture (et affiche un Buffer, car aucune encodage n’a été fournie) :
// Synchronous
const data = fs.readFileSync('file.txt');
console.log(data);
Dans la version asynchrone, la dernière ligne du journal d’erreurs s’affiche avant le contenu du fichier, car la lecture se termine lors d’un prochain cycle du cycle d’événements :
// Asynchronous
fs.readFile('file.txt', (err, data) => {
console.log(data);
});
console.log("This runs first!");
Q5. Qu’est-ce que npm ?
npm est le gestionnaire de paquets fourni avec Node.js. Vous l’utilisez pour installer, publier et gérer les dépendances, et il les enregistre, ainsi que les scripts, dans package.json.
Q6. package.json versus package-lock.json
package.json décrit le projet : son nom, sa version, son point d’entrée, les scripts et les plages de versions des dépendances qu’il accepte. package-lock.json fixe la version exacte de chaque paquet installé, y compris ceux transitoires, afin que chaque machine et chaque exécution CI installe un arbre identique. Committez les deux.
Q7. Quels modules de base devez-vous connaître ?
Les fonctions intégrées le plus souvent citées incluent fs, path, http et https, os, events, stream, buffer, crypto, util, url, querystring (considéré désormais comme obsolète au profit de URLSearchParams), child_process, cluster et zlib.
Q8. require() versus import
Les différences entre CommonJS et les modules ES sont soulevées constamment :
requirecharge les fichiers de manière synchrone ; les modules ES sont chargés de manière asynchrone et offrent également une fonctionimport()dynamique qui retourne une promesse.- CommonJS est la norme par défaut pour les fichiers
.js; les modules ES nécessitent l’indication"type": "module"danspackage.jsonou l’extension.mjs.
module.exports ; les modules ES emploient export et export default.require peut être appelé n’importe où, y compris à l’intérieur de conditions ; les déclarations statiques import sont relevées et doivent se trouver au niveau le plus élevé.Q9. module.exports versus exports
exports commence par faire référence au même objet que module.exports. Ajouter des propriétés à celui-ci fonctionne, mais assigner une nouvelle valeur (exports = {...}) ne fait que réassigner la variable locale, et le module continue d’exporter l’objet original. Pour remplacer tout l’export, assignez à module.exports.
Q10. Qu’est-ce que le REPL ?
REPL signifie Read, Eval, Print, Loop. L’exécution de node sans arguments ouvre un shell interactif qui évalue chaque expression que vous saisissez et affiche le résultat, ce qui est pratique pour des expériences rapides.
La boucle d’événements et les patterns asynchrones
Q11. Qu’est-ce que la boucle d’événements ?
C’est le mécanisme qui permet à une seule thread JavaScript d’effectuer des opérations d’entrée/sortie non bloquantes. Lorsque la pile d’appels est vide, la boucle prend le prochain callback prêt dans ses files d’attente et l’exécute. Une explication plus approfondie de sa manière d’interagir avec libuv se trouve dans notre article sur libuv, la boucle d’événements et le pool de threads.
Q12. Quelles sont les phases de la boucle d’événements ?
- Timers : callbacks provenant de
setTimeoutetsetIntervaldont le délai est arrivé.
setImmediate().socket.on('close').Les files d’attente de microtâches (process.nextTick en premier, puis les réactions de promesse) sont vidées après chaque appel individuel, et non seulement entre les phases. C’est le comportement depuis Node 11, et c’est une question fréquemment posée.
Q13. nextTick, promises, setTimeout et setImmediate
- Les appels de
process.nextTick()s’exécutent dès que l’opération en cours est terminée, avant les microtâches de promesse.
.then s’exécutent une fois la file d’attente nextTick vide, mais avant que le cycle ne passe à l’étape suivante.setTimeout(fn, 0) est déclenché pendant la phase des compteurs à rebours une fois son délai minimal écoulé.setImmediate() est déclenché pendant la phase de vérification, juste après la phase d’inspection de la même itération.Ce script met en œuvre les quatre méthodes :
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('end');
La sortie attendue montre d’abord les journaux synchrones, puis les microtâches. Les deux macrotâches peuvent apparaître dans n’importe quel ordre lorsqu’elles sont planifiées depuis le module principal, car cela dépend de savoir si le seuil du compteur à rebours est écoulé au démarrage du cycle ; à l’intérieur d’une fonction de rappel de fichier ou de socket, setImmediate l’emporte toujours :
// Output:
// start
// end
// nextTick
// promise
// setTimeout (or setImmediate — order can vary if not inside I/O)
// setImmediate (or setTimeout)
Q14. Appels de fonction et enfer des appels de fonction
Un callback est une fonction que l’on passe à une opération afin qu’elle puisse être appelée lorsque cette opération est terminée. Lorsque chaque étape dépend de la précédente, les callbacks s’empilent de plus en plus profondément, créant la pyramide connue sous le nom d’« enfer des callbacks », ce qui rend difficile la lecture du code ainsi que la gestion des erreurs :
getUser(id, (user) => {
getPosts(user, (posts) => {
getComments(posts, (comments) => {
// deeply nested - hard to manage
});
});
});
Les Promises et async/await aplanissent cette structure.
Q15. Qu’est-ce qu’une Promise ?
Une Promise est un objet qui représente le résultat futur d’une opération asynchrone. Elle peut être en attente, satisfaite avec une valeur ou rejetée pour une raison donnée ; une fois son état déterminé, elle ne change plus jamais. On y associe des gestionnaires d’événements avec .then et .catch:
const promise = new Promise((resolve, reject) => {
if (success) resolve(data);
else reject(error);
});
promise.then(data => console.log(data)).catch(err => console.error(err));
Q16. Qu’est-ce que async/await ?
C’est une syntaxe basée sur les promesses qui permet au code asynchrone d’être lu de haut en bas. Une fonction async renvoie toujours une promesse, et await pause cette fonction jusqu’à ce que la promesse attendue soit résolue, les rejets apparaissant alors sous forme d’exceptions que l’on peut capturer :
async function getUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
return data;
} catch (err) {
console.error(err);
}
}
Notez que cet exemple enregistre l’erreur et renvoie implicitement undefined, de sorte que les appels ne peuvent pas distinguer une échec d’un résultat vide. Relancer l’erreur est souvent le meilleur choix.
Q17. Combinateurs de promesses
Promise.all()se résout lorsque toutes les entrées sont satisfaites, et rejette dès que l’une d’elles échoue.Promise.race()se résout avec la première entrée à se résoudre, qu’elle soit satisfaite ou rejetée.Promise.allSettled()attend que toutes les entrées soient traitées et rapporte chaque résultat.
Promise.any() se satisfait du premier élément qui se satisfait et rejette (avec un AggregateError) uniquement si tous se rejetent.Q18. Blocant versus non bloquant
Le code blocant arrête tout le reste jusqu’à ce qu’il soit terminé, comme c’est le cas avec fs.readFileSync. Le code non bloquant commence le travail et permet au programme de continuer, comme c’est le cas avec fs.readFile. Sur un serveur, une appelation bloquante bloque toutes les requêtes en cours.
Flux, buffers et événements
Q19. Qu’est-ce que les flux ?
Les flux traitent les données de manière incrémentale, par tranches, au lieu de les conserver entièrement en mémoire. Ce sont l’outil idéal pour les gros fichiers, les médias et le trafic réseau. Il en existe quatre types :
- Lisibles, comme
fs.createReadStream - Écrivables, comme
fs.createWriteStream - Duplex, lisible et écrivable en même temps, comme un socket TCP
- Transform, un flux duplex qui modifie les données au fur et à mesure de leur transmission, comme
zlib.createGzip
Q20. Qu’est-ce qu’un Buffer ?
const buf = Buffer.from('Hello');
console.log(buf); // <Buffer 48 65 6c 6c 6f>
Q21. Qu’est-ce qu’EventEmitter ?
EventEmitter du module events met en œuvre le pattern d’observateur (pub/sub) sur lequel repose une grande partie de l’API Node. On crée une instance :
const EventEmitter = require('events');
const emitter = new EventEmitter();
puis on enregistre des écouteurs avec on et on les déclenche avec emit, en transmettant tous les arguments nécessaires :
emitter.on('greet', (name) => console.log(`Hello, ${name}`));
emitter.emit('greet', 'John');
Les écouteurs s’exécutent de manière synchrone, dans l’ordre de leur inscription, lorsque la méthode emit est appelée.
Q22. Comment traiter de grandes quantités de données sans épuiser la mémoire ?
Utilisez les flux : lisez de petits fragments et redirigez un flux lisible vers un flux écrivable, afin que la consommation de mémoire reste plus ou moins constante quel que soit la taille du fichier :
fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
Pour le code de production, stream.pipeline est préférable à .pipe() car il transmet les erreurs et nettoie chaque flux de la chaîne.
Concepts avancés liés au temps d’exécution
Q23. process.nextTick() versus setImmediate(), en détail
nextTick exécute ses fonctions de rappel avant que la boucle ne puisse continuer, même avant les microtâches de promesse. Les fonctions de rappel setImmediate attendent la phase de vérification, après la phase d’inspection. Comme chaque nextTick peut planifier un autre, une chaîne récursive peut paralyser la boucle de sorte que les lectures et écritures en attente ne soient jamais traitées. Ce mode de défaillance est abordé dans comment process.nextTick paralyse silencieusement la boucle d’événements de Node.js.
Q24. Qu’est-ce que le clustering et pourquoi l’utiliser ?
Un seul processus Node utilise un cœur pour le JavaScript. Le module cluster crée plusieurs processus travailleurs, chacun disposant de sa propre boucle d’événements et de sa propre mémoire, et leur permet de partager un même port serveur afin que tous les cœurs de la machine traitent le trafic. Vous importez les modules comme suit :
const cluster = require('cluster');
const os = require('os');
Ensuite, on crée un travailleur par CPU à partir du processus principal, tandis que les travailleurs chargent le serveur :
if (cluster.isMaster) {
os.cpus().forEach(() => cluster.fork());
} else {
require('./server');
}
cluster.isMaster fonctionne toujours, mais a été déprécié au profit de cluster.isPrimary ; utilisez ce dernier dans les nouveaux codes.
Q25. cluster versus worker_threads
clustercrée des processus séparés avec de la mémoire isolée, ce qui convient bien à l’extension des serveurs HTTP.worker_threadscrée des threads à l’intérieur d’un seul processus. Ils peuvent partager de la mémoire viaSharedArrayBufferou transférer des buffers, ce qui est adapté aux tâches gourmandes en CPU comme le traitement d’images.
Q26. Comment sont gérées les tâches gourmandes en CPU ?
Les blocs de code gourmands en CPU bloquent la boucle d’événements, il faut donc les déplacer ailleurs : vers worker_threads, dans un processus enfant, ou complètement en dehors du service via une file d’attente et des travailleurs en arrière-plan. Consultez comment transférer les tâches gourmandes en CPU à l’aide de worker threads et de pools pour découvrir des modèles concrets.
Q27. Qu’est-ce que le middleware en général ?
Le middleware est du code qui se situe entre la réception d’une requête et l’envoi de la réponse, où il peut inspecter, transformer, valider ou enregistrer des informations. Express repose entièrement sur ce principe, comme le montrent les sections suivantes consacrées à Express.
Q28. Qu’est-ce que les processus enfants ?
Le module child_process exécute des programmes externes ou d’autres scripts Node dans des processus distincts. Quatre fonctions sont particulièrement importantes :
spawn()permet le streaming des résultats, ce qui lui permet de gérer efficacement les grandes quantités de données générées.
exec() exécute un shell et met en mémoire tamponne les résultats, ce qui convient pour de petits volumes de données.execFile() exécute directement un fichier sans passer par un shell, ce qui est plus sûr avec des arguments non fiables.fork() est une version spécialisée de spawn pour les modules Node qui crée un canal IPC pour l’échange de messages.Q29. Qu’est-ce que libuv ?
libuv est la bibliothèque en C qui fournit le cycle d’événements de Node, les opérations d’entrée/sortie asynchrones ainsi qu’un pool de threads, en masquant les différences entre les mécanismes de notification d’événements sous Linux, macOS et Windows.
Q30. setTimeout versus setInterval
setTimeout(fn, delay) exécute fn une seule fois après au moins delay millisecondes. setInterval(fn, delay) la exécute de manière répétée à cet intervalle jusqu’à ce que vous appeliez clearInterval().
Q31. Gestion des exceptions non capturées et des rejets non traités
Il existe des gestionneurs au niveau du processus pour les deux cas. Pour les exceptions synchrones non capturées, il faut enregistrer un message d’erreur et arrêter le processus, car celui-ci peut se trouver dans un état incohérent :
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1); // recommended: exit after cleanup
});
Pour les promesses rejetées sans gestionneur :
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
Depuis Node 15, un rejet non traité provoque par défaut l’arrêt du processus ; enregistrer ce gestionneur modifie ce comportement. Considérez ces mécanismes comme une solution de dernier recours pour l’affichage de messages, et non comme un outil de contrôle du flux de programme ; utilisez plutôt try/catch et .catch() directement dans le code.
Q32. Qu’est-ce qu’une fuite mémoire et comment la détecter ?
Une fuite mémoire correspond à de la mémoire encore accessible après qu’elle n’est plus nécessaire : des caches mondiaux en expansion, des temporiseurs ou des écouteurs jamais supprimés, des closures conservant de gros objets. Diagnostiquez cela à l’aide du flag --inspect et des captures d’écran de la pile de Chrome DevTools, ou d’outils tels que heapdump et clinic.js, en comparant les captures au fil du temps.
Q33. __dirname versus __filename
__dirname représente le chemin absolu du dossier contenant le module actuel ; __filename correspond au chemin absolu du fichier lui-même. Ces deux valeurs n’existent que dans CommonJS. Dans les modules ES, obtenez-les à partir de import.meta.url, ou utilisez import.meta.dirname et import.meta.filename sur les versions récentes de Node.
Q34. Pourquoi utiliser le module path ?
path permet de construire et d’analyser des chemins de fichiers de manière portable à l’aide de fonctions telles que path.join, path.resolve, path.basename et path.extname ; ainsi, vous n’avez jamais à concaténer manuellement des chaînes et à vous heurter aux barres obliques de Windows par rapport aux barres obliques d’Unix.
Q35. fs.readFile versus fs.createReadStream
fs.readFile charge tout le fichier en mémoire avant de rappeler la fonction, ce qui est risqué pour les fichiers volumineux. fs.createReadStream fournit le fichier par tranches, ce qui permet de maintenir une consommation mémoire faible et de commencer le traitement immédiatement.
Essentiels d’Express
Q36. Qu’est-ce qu’Express ?
Express est un framework web minimaliste et neutre, basé sur le module http de Node. Il ajoute du routage, un pipeline de middleware ainsi que l’intégration d’un moteur de templates, éliminant la majeure partie du code générique lié à la gestion brute des requêtes.
Q37. Un serveur Express de base
Créez un objet d’application :
const express = require('express');
const app = express();
puis enregistrez une route et commencez à écouter :
app.get('/', (req, res) => {
res.send('Hello World');
});app.listen(3000, () => console.log('Server running on port 3000'));
Q38. Qu’est-ce que le middleware dans Express ?
Un middleware d’Express est une fonction qui reçoit req, res et next. Elle peut exécuter n’importe quel code, modifier la requête ou la réponse, terminer le cycle en répondant, ou appeler next() pour transférer le contrôle à la fonction suivante. Un journaliseur de requêtes en est un exemple classique :
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
Si un middleware ne répond pas ni n’appelle next(), la requête reste bloquée.
Q39. Types de middleware Express
- Niveau application, attaché avec
app.use()ouapp.METHOD(). - Niveau routeur, attaché à une instance d’
express.Router().
(err, req, res, next).express.json() et express.static().cors, morgan et helmet.Q40. Comment fonctionne le routage ?
app.get('/users', (req, res) => { /* get all users */ });
app.post('/users', (req, res) => { /* create user */ });
app.put('/users/:id', (req, res) => { /* update user */ });
app.delete('/users/:id', (req, res) => { /* delete user */ });
Q41. Qu’est-ce que express.Router() ?
// userRoutes.js
const router = require('express').Router();
router.get('/', (req, res) => res.send('All users'));
module.exports = router;
et installez-le dans l’application principale, où son / devient /users :
// app.js
app.use('/users', require('./userRoutes'));
Q42. app.use() par rapport à app.get() et aux autres fonctions similaires
app.use() correspond à chaque méthode HTTP ainsi qu’à tout chemin commençant par le préfixe indiqué, c’est pourquoi il permet d’installer des middleware et des routeurs. app.get(), app.post() et similaires correspondent à une méthode spécifique ainsi qu’au schéma complet du chemin.
Q43. req.params, req.query et req.body
req.paramscontient les segments de chemin nommés :/users/:iddonnereq.params.id.req.querycontient les valeurs de la chaîne de requête :/users?age=25donnereq.query.age, toujours sous forme de chaîne de caractères.req.bodycontient le chargement du payload de la requête pour des méthodes comme POST et PUT, et reste défini sur null sauf si un middleware de parsing du corps, tel queexpress.json(), a été exécuté en premier.
Q44. Parsing des données JSON et de formulaire
Deux analyseurs intégrés couvrent la plupart des cas : l’un pour les corps JSON et l’autre pour les formulaires HTML codés en URL :
app.use(express.json()); // parses application/json
app.use(express.urlencoded({ extended: true })); // parses form data
Q45. Qu’est-ce que express.static() ?
C’est un middleware intégré qui sert des fichiers tels que des images, des feuilles de style, des scripts et du HTML depuis un répertoire :
app.use(express.static('public'));
Express intermédiaire
Q46. Gestion des erreurs
Les erreurs sont gérées par un middleware à quatre paramètres, enregistré après toutes les routes :
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong' });
});
Dans les routes et le middleware, transmettez les échecs à ce dernier avec next(err) :
app.get('/user/:id', async (req, res, next) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (err) {
next(err); // passed to error-handling middleware
}
});
Évitez d’envoyer err.stack ou des messages internes aux clients en production.
Q47. Gestion des 404
Express ne dispose d’aucun mécanisme intégré pour les 404 en JSON. Ajoutez un middleware catch-all après chaque route ; si une requête y parvient, rien d’autre n’a correspondu :
app.use((req, res) => {
res.status(404).json({ message: 'Route not found' });
});
Q48. CORS et le package cors
Cross-Origin Resource Sharing est le mécanisme du navigateur qui détermine si une page provenant d’une origine peut lire des réponses en provenance d’une autre. Le navigateur l’impose ; le serveur s’y conforme via des en-têtes de réponse, que le paquet cors configure à votre place :
const cors = require('cors');
app.use(cors()); // allow all origins
app.use(cors({ origin: 'https://example.com' })); // restrict origin
Cette partie montre deux alternatives : autoriser toutes les origines ou une seule ; utilisez l’une ou l’autre, mais pas les deux, car la configuration permissive s’appliquera en premier.
Q49. Sessions et cookies
Chargez les modules intermédiaires pour les cookies et les sessions :
const cookieParser = require('cookie-parser');
const session = require('express-session');
puis enregistrez-les avec un secret de signature et des options :
app.use(cookieParser());
app.use(session({
secret: 'mySecretKey',
resave: false,
saveUninitialized: true
}));
Gardez le secret dans une variable d’environnement plutôt que dans le code source, et envisagez de définir saveUninitialized: false pour éviter la création de sessions pour les visiteurs anonymes. Le stockage en mémoire par défaut n’est pas conçu pour un environnement de production.
Q50. Authentification JWT
Importer la bibliothèque :
const jwt = require('jsonwebtoken');
Le code ci-dessous signe un jeton contenant l’ID de l’utilisateur lors de la connexion, protège les routes à l’aide d’un middleware qui extrait le jeton porteur de l’en-tête Authorization et le vérifie, puis associe la charge utile décodée à req.user :
// Generate token on login
const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'No token' });
try {
req.user = jwt.verify(token, 'secretKey');
next();
} catch (err) {
res.status(403).json({ message: 'Invalid token' });
}
}app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
Tout comme pour les sessions, la valeur 'secretKey' codée en dur devrait provenir de la configuration. Une réponse 401 en cas d’absence de jeton et une réponse 403 en cas de jeton invalide constituent une convention courante, bien que de nombreuses API retournent 401 pour les deux cas.
Q51. Validation de la requête
Des bibliothèques telles que express-validator ou joi déclarent des règles et collectent les erreurs. Avec express-validator, on importe les outils d’aide :
const { body, validationResult } = require('express-validator');
puis on enchaîne les middleware de règles avant le gestionnaire et on vérifie le résultat :
app.post('/user',
body('email').isEmail(),
body('password').isLength({ min: 6 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// proceed
}
);
Pour une comparaison plus approfondie des méthodes de validation, consultez Zod versus express-validator.
Q52. Téléchargement de fichiers
multer analyse les données du type multipart/form-data. Il faut configurer une destination :
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
et l’appliquer par route ; upload.single('file') attend un seul fichier dans le champ nommé file et le met à disposition sous la forme req.file:
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ file: req.file });
});
Dans les systèmes réels, il convient également de limiter la taille et le type des fichiers, et de ne jamais faire confiance au nom de fichier fourni par le client.
Q53. Connexion à MongoDB avec Mongoose
Ouvrez une connexion :
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb');
puis définez un modèle et utilisez-le depuis une route :
const User = mongoose.model('User', new mongoose.Schema({
name: String,
email: String
}));app.post('/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
En passant directement req.body à la fonction create, les clients peuvent définir n’importe quel champ du schéma ; il est donc nécessaire de valider ou de sélectionner d’abord les champs autorisés.
Q54. Ordre d’exécution des middleware
Express exécute les middleware dans l’ordre où ils ont été enregistrés, de haut en bas, et la chaîne s’arrête à toute fonction qui ne lance pas next(). Cet ordre explique pourquoi les gestionnaires 404 et les gestionnaires d’erreurs sont placés en dernier.
Q55. Moteurs de templates
Les moteurs de templates génèrent du HTML à partir de données serveur. Les choix populaires sont EJS, Pug (anciennement Jade) et Handlebars. Pour les mettre en place, il suffit d’une configuration du moteur de vues et d’un appel à res.render :
app.set('view engine', 'ejs');
app.get('/', (req, res) => res.render('index', { name: 'John' }));
Express et Node avancés
Q56. Erreurs asynchrones dans Express 4 par rapport à Express 5
Dans Express 4, une promesse rejetée provenant d’un gestionnaire async n’est pas capturée par le framework ; sans try/catch accompagné de next(err) ou d’un wrapper, cela devient un rejet non géré. Express 5 transmet automatiquement les promesses rejetées provenant des gestionnaires et du middleware au gestionnaire d’erreurs. La solution habituelle en Express 4 consiste à utiliser une petite fonction de haut niveau :
// Express 4 workaround
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
En enveloppant un gestionnaire avec cette fonction, tout rejet est redirigé vers next:
app.get('/data', asyncHandler(async (req, res) => {
const data = await fetchData(); // if this throws, asyncHandler catches it
res.json(data);
}));
Cette approche est expliquée en détail dans removing try/catch boilerplate with asyncHandler.
Q57. Structurer une grande application Express
Un schéma typique de style MVC sépare les différentes fonctions dans des dossiers :
project/
├── controllers/ # business logic
├── routes/ # route definitions
├── models/ # DB schemas
├── middlewares/ # custom middleware
├── config/ # DB, env configs
├── utils/ # helper functions
├── app.js
└── server.js
Les routes déclarent des points d’entrée et redirigent vers des contrôleurs, les contrôleurs gèrent la logique des requêtes, tandis que les modèles encapsulent l’accès aux données. Séparer app.js (qui configure l’application) de server.js (qui la lance) facilite également son importation dans les tests.
Q58. Limitation des débits
express-rate-limit compte les requêtes par client sur une période donnée. Importez-le :
const rateLimit = require('express-rate-limit');
et appliquez un limiteur, ici 100 requêtes par IP toutes les 15 minutes :
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per window
});app.use(limiter);
Le stockage par défaut se fait en mémoire, ce qui signifie que chaque processus gère les comptes séparément ; utilisez un stockage partagé comme Redis lorsque vous scalez l’application. Derrière un proxy, configurez trust proxy afin que l’IP réelle du client soit utilisée.
Q59. Sécurisation d’une application Express
- Definissez des en-têtes HTTP de protection avec
helmet.
dotenv, plutôt que de les stocker dans le code.npm audit.Q60. res.send() versus res.json() versus res.end()
res.send()accepte des chaînes de caractères, des objets ou des buffers et choisit unContent-Typeapproprié.res.json()sérialise son argument en JSON et définitContent-Type: application/json.res.end()termine la réponse sans données supplémentaires ; il s’agit d’un enveloppement simple autour de la méthode native de Node.
Q61. Que fait next('route') ?
Cela saute les appels de fonction restants de la route actuelle et passe directement à la prochaine route correspondante. Cela ne s’applique qu’aux gestionnaires enregistrés via app.METHOD() ou router.METHOD() qui disposent de plusieurs appels de fonction.
Q62. Améliorer les performances
- Exécutez un processus par cœur en utilisant des outils comme clustering ou PM2.
- Mémorisez les réponses dans Redis ou en mémoire.
- Comprimez les réponses à l’aide du middleware
compression, ou laissez un proxy s’en charger. - Mettez Nginx ou un autre proxy inversé ainsi qu’un équilibreur de charge en amont.
- Éloignez les tâches gourmandes en CPU du cycle d’événements.
- Gérez les connexions à la base de données en pool.
- Envisagez l’utilisation d’HTTP/2.
- Utilisez des entrées/sorties asynchrones partout et évitez les variantes
Syncdans les chemins des requêtes.
Q63. REST contre GraphQL avec Express
REST expose de nombreux points d’entrée présentant des structures de réponse fixes, ce qui pousse souvent les clients à demander plus ou moins de données que nécessaire. GraphQL expose un seul point d’entrée où le client peut décrire précisément les données qu’il souhaite obtenir. Il est généralement intégré à Express à l’aide de express-graphql ou apollo-server-express ; ces deux packages ont depuis été dépréciés au profit d’intégrations plus récentes, il convient donc de consulter la documentation actuelle d’Apollo ou de GraphQL Yoga.
Q64. Comment Express gère-t-il les requêtes simultanées ?
Chaque requête n’est rien de plus qu’une série d’appels en retour dans le cycle d’événements. Comme les appels à la base de données et au réseau ne bloquent pas l’exécution, un seul thread peut gérer de nombreuses requêtes en parallèle : le trafic réseau est confié aux mécanismes asynchrones du système d’exploitation, le traitement des fichiers au pool de threads libuv, et les appels en retour s’exécutent dès que les résultats arrivent. Contrairement aux serveurs multi-threadés classiques où il y a un thread par requête, cela explique également pourquoi un gestionnaire synchrone lent affecte tout le système.
Q65. PM2 versus le module cluster
cluster est la primitive intégrée pour l’extension en multi-processus. PM2 est un gestionnaire de processus basé sur la même idée, qui ajoute des redémarrages automatiques après panne, un équilibrage de charge, une gestion des journaux, des mises à jour sans interruption et un suivi en temps réel, ce qui explique sa présence dans de nombreuses implémentations en production.
Q66. Pourquoi placer un proxy inversé devant Node ?
Q67. Configuration spécifique à l’environnement
dotenv charge les variables depuis un fichier .env dans process.env, et l’application recourt aux valeurs par défaut lorsque une valeur fait défaut :
require('dotenv').config();
const PORT = process.env.PORT || 3000;
--env-file. Ne communiquez jamais de fichiers .env contenant des informations secrètes.
Q68. app.all() versus app.use()
app.all() correspond à chaque méthode HTTP sur un chemin de route spécifique. app.use() correspond également à chaque méthode, mais pour le chemin et tout ce qui se trouve en dessous, c’est pourquoi il est utilisé pour intégrer des middleware.
Q69. Pagination avec Express et MongoDB
Lisez les valeurs de page et limit à partir de la chaîne de requête en utilisant des valeurs par défaut raisonnables, puis combinez skip et limit:
app.get('/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const posts = await Post.find()
.skip((page - 1) * limit)
.limit(limit);
res.json(posts);
});
Rendez ce code plus robuste avant déploiement : passez un radicande à parseInt, restreignez page à au moins 1 et limit à une valeur maximale, et ajoutez un critère de tri sort fiable. Pour des collections très volumineuses, la vitesse de skip diminue à mesure que l’offset augmente, tandis que la pagination basée sur des curseurs sur un champ indexé fonctionne mieux.
Q70. Qu’est-ce que l’idempotence ?
Une opération est idempotente lorsqu’en la répétant, le serveur reste dans le même état qu’après une exécution unique. GET, PUT et DELETE sont définis comme idempotents ; POST ne l’est généralement pas, car chaque appel peut créer une nouvelle ressource. Les tentatives de réexécution après expiration ne sont sûres que pour les requêtes idempotentes, c’est pourquoi les API de paiement ajoutent souvent des clés d’idempotence aux requêtes POST.
Points clés
- La plupart des questions concernant Node se résument à un modèle : une seule thread JavaScript, un cycle d’événements avec des phases définies, les microtâches éliminées après chaque appel de callback, et libuv s’occupant du reste.
- La plupart des questions concernant Express se résument à un autre modèle : une chaîne ordonnée de middleware où chaque fonction soit répond, soit appelle
next(), les erreurs étant transmises aux gestionnaires à quatre arguments.
cluster.isPrimary, les plantages en cas de rejet non géré, les erreurs asynchrones d’Express 5 et les outils de chemins ESM.