Node.js est un environnement d'exécution JavaScript en dehors du navigateur, construit sur V8 et étendu avec des API pour le réseau, les fichiers, les processus, les flux (streams) et l'accès au système d'exploitation. Il est couramment utilisé pour les API REST, les services GraphQL, les serveurs WebSocket temps réel, les microservices, les outils CLI, les proxys et les passerelles API (API gateways). Sa principale force réside dans les opérations I/O non bloquantes : un seul processus peut gérer de nombreuses connexions réseau efficacement. Le compromis est que les calculs longs et gourmands en CPU peuvent bloquer la boucle d'événements, à moins qu'ils ne soient déplacés vers des workers, des processus ou un autre service.
JavaScript s'exécute dans V8, tandis que les API de Node.js délèguent le travail asynchrone au système d'exploitation ou au pool de threads de libuv. Le thread JavaScript principal n'attend pas qu'une opération I/O soit terminée ; il continue d'exécuter d'autres codes. Lorsque l'opération est terminée, son callback ou sa continuation Promise devient prêt pour la boucle d'événements. C'est ainsi que Node.js atteint une haute concurrence même si le JavaScript utilisateur s'exécute généralement sur un thread principal.
3Qu'est-ce que V8, libuv et la boucle d'événements ?
V8 compile et exécute JavaScript et gère les objets et la mémoire JavaScript. libuv fournit la boucle d'événements multiplateforme, l'intégration I/O asynchrone et un pool de threads workers pour des opérations telles que certaines tâches de système de fichiers, DNS et crypto. La boucle d'événements décide quand les rappels des opérations terminées doivent s'exécuter. Node.js lui-même connecte les API JavaScript aux capacités natives ; V8 seul ne fournit pas de serveur HTTP, d'accès au système de fichiers ou d'API de processus.
4Pourquoi Node.js est souvent appelé mono-threadé ?
Un seul processus Node.js exécute généralement le JavaScript utilisateur sur un thread principal, de sorte que deux gestionnaires de requêtes normaux n'exécutent pas de JavaScript au même moment dans ce thread. Cependant, la plateforme n'est pas littéralement un seul thread : libuv dispose d'un pool de threads, le système d'exploitation effectue des I/O asynchrones, V8 a du travail de ramasse-miettes (garbage-collection), et Node.js prend également en charge worker_threads et les processus enfants. Cette expression décrit principalement le modèle d'exécution JavaScript par défaut.
5Comment Node.js peut-il gérer de nombreuses requêtes en même temps ?
Node.js lance les opérations I/O, puis revient à la boucle d'événements au lieu de bloquer un thread par requête. Par exemple, 1 000 appels à la base de données peuvent être en cours pendant que le thread principal continue d'accepter des connexions et de traiter les rappels prêts. Cela fonctionne bien pour les charges de travail liées aux E/S (I/O-bound) telles que les appels HTTP, les requêtes de base de données, les fichiers et les sockets. Il fonctionne mal lorsque les gestionnaires de requêtes effectuent un lourd travail CPU synchrone, car cela bloque la boucle d'événements et retarde toutes les autres requêtes.
6Quelles sont les principales phases de la boucle d'événements de Node.js ?
Node.js utilise les phases de planification de libuv, notamment pending callbacks, idle/prepare, poll, check et close callbacks, ainsi que le traitement des temporisateurs. poll traite de nombreux rappels d'entrées-sorties, check exécute les rappels setImmediate, et les temporisateurs exécutent les rappels setTimeout/setInterval lorsque leurs délais minimaux et la planification le permettent. À partir de libuv 1.45 et de Node.js 20, les temporisateurs sont traités après poll à chaque itération, avec un premier passage de compatibilité avant l'entrée dans la boucle. process.nextTick utilise une file distincte de Node, tandis que les réactions aux Promise et queueMicrotask utilisent la file de microtâches de V8 ; aucune de ces files n'est une phase de libuv. Elles sont traitées aux points de contrôle entre les rappels. Les alimenter récursivement peut empêcher les autres tâches de s'exécuter.
7Quelle est la différence entre les opérations synchrones, asynchrones et non bloquantes ?
Synchrone décrit le flux de contrôle : un appel produit son résultat ou lève une exception avant que l'appelant continue. Asynchrone décrit un résultat fourni ultérieurement par un rappel, une Promise ou une autre notification. Bloquant signifie attendre un travail ou une ressource pendant que le thread courant ne peut rien faire d'autre ; une interface non bloquante évite cette attente, par exemple en renvoyant un résultat « bloquerait » ou en organisant une notification. Ce sont des dimensions différentes : synchrone ne signifie pas à lui seul qu'une interface attend la disponibilité d'une entrée-sortie, et renvoyer une Promise ne rend pas non bloquant un calcul synchrone lourd. Dans Node.js, fs.readFileSync est une interface synchrone bloquante ; fs.promises.readFile fournit son résultat de manière asynchrone pendant que le thread principal peut traiter d'autres tâches. Privilégie les interfaces d'entrées-sorties asynchrones adaptées dans le traitement des requêtes.
8Qu'est-ce que la famine de la boucle d'événements ?
La famine de la boucle d'événements survient lorsque du JavaScript synchrone ou des files de points de contrôle constamment réalimentées empêchent de traiter à temps les rappels d'entrées-sorties, les temporisateurs et les requêtes. Elle peut provenir de calculs lourds, de grandes opérations JSON, de microtâches planifiées récursivement et d'un usage excessif de process.nextTick. Limite la taille des entrées, mesure les traitements coûteux avec un profileur et déplace les tâches gourmandes en calcul vers worker_threads ou un autre service. Si tu découpes le travail, rends la main à une itération ultérieure de la boucle, par exemple avec setImmediate ; attendre répétitivement des Promise déjà résolues ou utiliser queueMicrotask peut encore priver les entrées-sorties de temps d'exécution. Évite les interfaces synchrones bloquantes dans les chemins fréquemment empruntés par les requêtes.
9Qu'est-ce qu'une fonction de rappel et qu'appelle-t-on l'enfer des rappels ?
Une fonction de rappel est transmise à une autre opération pour que celle-ci l'invoque. Elle peut être exécutée de manière synchrone, comme avec Array.map, ou asynchrone, comme avec fs.readFile. Les rappels asynchrones classiques de Node.js suivent souvent la convention de l'erreur en premier argument : callback(error, result). L'enfer des rappels désigne des rappels dépendants profondément imbriqués, qui rendent le flux de contrôle et la gestion des erreurs difficiles à suivre. Des fonctions nommées, des opérations plus petites, les Promise et async/await améliorent la lisibilité. Lance ensemble les opérations indépendantes lorsque cela convient, par exemple avec Promise.all, en respectant les limites de concurrence et en gérant les erreurs.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
10Qu'est-ce qu'une Promise et comment fonctionne async/await ?
Une Promise représente le résultat futur d'une opération asynchrone et peut être en attente (pending), remplie (fulfilled) ou rejetée (rejected). Une fonction async retourne toujours une Promise. await ne suspend que la fonction async actuelle jusqu'à ce que la Promise se stabilise ; cela ne bloque pas tout le processus Node.js. Une bonne réponse devrait mentionner la gestion des erreurs avec try/catch, éviter les Promises flottantes et utiliser Promise.all pour un travail indépendant afin que les opérations ne s'exécutent pas inutilement de manière séquentielle.
11Quelle est la différence entre process.nextTick, queueMicrotask, setImmediate et setTimeout ?
process.nextTick planifie du travail dans la file next-tick de Node ; queueMicrotask utilise la file de microtâches de V8, également employée par les réactions aux Promise. À un point de contrôle habituel au niveau supérieur de CommonJS, les rappels nextTick s'exécutent avant les rappels Promise/queueMicrotask en attente. Au niveau supérieur d'un module ES, l'évaluation a déjà lieu dans un contexte de microtâche : les microtâches en attente peuvent donc précéder nextTick. Cela dépend du contexte ; ce n'est pas une règle universelle de priorité qui interrompt le JavaScript en cours. setImmediate s'exécute dans check. setTimeout(fn, 0) attend un délai minimal et n'est pas immédiat. Leur ordre relatif dépend de l'endroit où ils sont planifiés ; si les deux le sont dans un rappel d'entrée-sortie, setImmediate passe en premier. Privilégie queueMicrotask pour différer une opération en microtâche de façon portable, sauf si le comportement propre à nextTick est nécessaire.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
12Comment faut-il gérer les erreurs dans Node.js ?
Utilise try/catch pour les exceptions synchrones. Pour les opérations fondées sur les Promise, place await dans try/catch ou ajoute un gestionnaire de rejet ; un try/catch synchrone extérieur n'intercepte pas l'exception lancée par un rappel exécuté plus tard. Les rappels dont le premier argument est l'erreur doivent vérifier cet argument, et les événements error d'EventEmitter nécessitent des gestionnaires adaptés. Les serveurs doivent centraliser la gestion des erreurs de requêtes, journaliser un contexte utile et éviter d'exposer les détails internes aux clients. Après une uncaughtException, ne suppose pas que le processus peut continuer sans danger : effectue uniquement le nettoyage d'urgence synchrone indispensable, termine le processus et laisse un superviseur le redémarrer. Un arrêt propre planifié est utile pour une terminaison normale, mais une récupération asynchrone arbitraire après une exception non interceptée n'est pas une stratégie fiable.
Un module est une unité de code isolée avec une interface exportée explicite. Node.js prend en charge des modules intégrés tels que node:fs et node:http, les modules locaux du projet, les paquets npm, les modules CommonJS et les modules ECMAScript. Les modules clarifient les frontières de responsabilité et permettent la réutilisation. Le compromis en production est le couplage : si les modules s'importent trop profondément ou masquent des effets secondaires au moment de l'importation, il devient plus difficile d'analyser les comportements de test et de démarrage.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Quelle est la différence entre require de CommonJS et import des modules ES ?
CommonJS utilise require et module.exports ; les modules ES utilisent import et export. require charge normalement de manière synchrone et peut être appelé conditionnellement. Les déclarations import statiques rendent le graphe de dépendances analysable ; import() dynamique renvoie une Promise. Les versions modernes de Node.js peuvent également charger avec require un graphe de modules ES sans await au niveau supérieur, selon les règles d'interopérabilité prises en charge. Les modules ES ne sont donc pas synonymes d'évaluation toujours asynchrone. Les extensions .cjs/.mjs, le champ type de package.json et les règles applicables de détection syntaxique déterminent le mode du module. Les modules ES permettent await au niveau supérieur. CommonJS fournit __dirname/__filename ; les versions actuelles de Node.js fournissent import.meta.dirname/filename pour les modules ES fondés sur des fichiers. import.meta.url sert à la résolution par URL et aux approches compatibles avec les versions plus anciennes.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
15Que sont npm, package.json et package-lock.json ?
npm est l'interface en ligne de commande de gestion des paquets. package.json décrit les métadonnées, les scripts et les exigences concernant les dépendances directes, souvent sous forme de plages de versions. package-lock.json enregistre un arbre de dépendances résolu avec les versions des paquets et des informations d'intégrité. Versionne le fichier de verrouillage d'une application. npm ci est utile en intégration continue : il installe à partir du fichier de verrouillage, supprime le répertoire node_modules existant et échoue si package.json et le verrouillage divergent. Un fichier de verrouillage réduit les dérives des dépendances ; il ne rend pas identiques les systèmes d'exploitation, les compilations natives, les scripts de cycle de vie, les versions de Node ou les options d'installation. Des compilations reproductibles exigent aussi une chaîne d'outils et un environnement maîtrisés.
16Comment gérer la configuration et les variables d'environnement ?
Lis les variables d'environnement via process.env : les valeurs présentes sont des chaînes, tandis qu'une propriété absente vaut undefined. Valide les valeurs requises et convertis-les explicitement au démarrage ; convertir PORT avec Number ne suffit pas à rejeter une chaîne vide, NaN ou un port hors limites. Échoue avec un message clair si la configuration requise est invalide. Ne conserve pas de véritables secrets dans le dépôt de code ; fournis les secrets de production via la plateforme de déploiement ou un gestionnaire de secrets. Une configuration d'exemple sans secrets peut être versionnée. NODE_ENV est une convention applicative, pas une frontière de sécurité fiable.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter implémente un modèle de publication/abonnement (publish-subscribe) à l'intérieur d'un processus Node.js. `on` enregistre un écouteur, `once` enregistre un écouteur pour un événement unique, `off` supprime un écouteur et `emit` appelle les écouteurs de manière synchrone dans l'ordre d'enregistrement. La nature synchrone est importante : un écouteur lent bloque l'émetteur (emitter). L'événement d'erreur est spécial car un événement d'erreur non géré peut faire planter le processus. Les systèmes à longue durée de vie doivent également supprimer les écouteurs inutilisés pour éviter les fuites de mémoire.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer représente une séquence de bytes de taille fixe. Il est utilisé pour les fichiers, les sockets TCP, les corps HTTP, les images, la cryptographie et les protocoles binaires. Buffer.from crée un tampon à partir de données existantes, Buffer.alloc crée une mémoire remplie de zéros, et Buffer.allocUnsafe est plus rapide mais peut contenir d'anciennes données jusqu'à ce qu'elles soient écrasées. Cela ne rend allocUnsafe approprié que lorsque le code remplit immédiatement le tampon et n'expose jamais de données périmées.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
19Qu'est-ce que les flux (streams) et quels types existent ?
Les flux traitent les données par morceaux au lieu de tout charger en mémoire. Les principaux types sont Readable, Writable, Duplex et Transform. Des exemples courants incluent les fichiers, les objets de requête et de réponse HTTP, les sockets TCP et les flux gzip. Les flux sont précieux pour les gros fichiers et le trafic réseau car ils réduisent l'utilisation de la mémoire et la latence. Une réponse complète doit mentionner que les erreurs de flux doivent être gérées correctement sur toute la chaîne.
20Qu'est-ce que la backpressure et pourquoi le pipeline est-il utile ?
La backpressure se produit lorsqu'une source de données produit des morceaux plus rapidement que la destination ne peut les consommer. Sans backpressure, la mémoire peut croître jusqu'à ce que le processus ralentisse ou plante. Les flux (Streams) et les tuyaux (pipe) peuvent coordonner le débit, mais pipeline provenant de node:stream/promises est plus sûr pour les chaînes multi-étapes car il propage les erreurs, ferme les flux associés et renvoie une Promise. C'est un bon choix par défaut pour la compression de fichiers, les téléversements, les téléchargements et les transformations de flux.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);