🟢 Node.js — du débutant au senior
Parcours complet pour maîtriser Node.js comme un ninja : runtime, I/O, concurrence, frameworks, qualité, production. Versions 18 → 20 → 22 → 24.
TL;DR — Node, ce n'est pas « JavaScript côté serveur ». C'est V8 (le moteur qui exécute ton JS sur un seul thread) marié à libuv (la lib C qui orchestre une boucle d'événements et un threadpool pour faire de l'I/O non bloquante). Tout le reste — streams, HTTP, frameworks, observabilité — découle de ce modèle. Le passage de dev à senior tient en une phrase : comprendre ce qui s'exécute sur le thread JS, ce qui part ailleurs (kernel, threadpool, workers), et comment ne jamais bloquer le premier. Ce parcours te fait descendre du runtime (
event loop,libuv) jusqu'à la prod (graceful shutdown, OpenTelemetry, supply chain), avec à chaque étage le mental model, les pièges, et le « comment un staff engineer raisonne ».
🧠 Le mental model qui tient tout le parcours
Avant de cliquer sur un lien, ancre cette image. Toute la suite n'est qu'un zoom sur l'une de ses boîtes.
┌──────────────────────────────────────────────────────────────┐
│ TON PROCESS NODE │
│ │
│ ┌───────────────────────┐ ┌─────────────────────────┐ │
│ │ Thread JS (V8) │ │ libuv │ │
│ │ - exécute TON code │◄────►│ - event loop (6 phases)│ │
│ │ - 1 seul à la fois │ │ - threadpool (4 def.) │ │
│ │ - call stack │ │ fs, crypto, dns, zlib│ │
│ │ - microtasks: │ └───────────┬─────────────┘ │
│ │ nextTick → Promise │ │ │
│ └───────────────────────┘ │ │
│ │ │
│ Réseau (net/http/tcp) ───────────────►│ kernel async │
│ ne passe PAS par le threadpool epoll/kqueue/IOCP│
└──────────────────────────────────────────────────────────────┘
│ │
▼ ▼
worker_threads (autre V8, autre loop) cluster / child_process
pour le CPU-bound, mémoire isolée (autres process, partage socket)Les trois questions qu'un senior se pose devant n'importe quel bout de code Node :
- Est-ce que ça bloque le thread JS ? Une boucle CPU, un
JSON.parsede 50 Mo, unfs.readFileSync, unbcryptsynchrone → toute l'app gèle. Mesure le lag avecmonitorEventLoopDelay. - Où part le travail async ? Threadpool (fs, crypto, dns.lookup, zlib — saturable, taille
UV_THREADPOOL_SIZE) ou kernel (réseau — quasi illimité) ? Ça change ta stratégie de scaling. - Qui draine la backpressure ? En streaming, si le consommateur est lent, qui ralentit le producteur ? Si personne → la RAM explose. C'est
.pipe()/pipeline()qui répond.
Garde ces trois questions en tête : elles reviennent à chaque niveau ci-dessous.
🗺️ Comment parcourir ce hub
| Si tu es… | Commence par | Puis | Ne saute pas |
|---|---|---|---|
| Dev JS qui découvre le backend Node | Niveau 1 (event loop, modules) | Niveau 2 (HTTP, fetch) | Streams (Niv. 1) — c'est le concept le plus sous-estimé |
| Dev Express confirmé qui vise senior | Niveau 1 §1 + §4, Niveau 3 entier | Niveau 6 (prod) | Memory/GC & profiling (Niv. 3) |
| Migration vers Node récent | Niveau 6 §4 (versions 18→24) | Niveau 5 §2 (TS natif) | ESM/dual packaging (Niv. 1 §3) |
| Prépa entretien senior | Les blocs 🎤 En entretien de chaque leçon | Niveau 3 entier | Event loop — la question piège n°1 |
Règle d'or du parcours : ne lis pas linéairement comme un livre. Pour chaque leçon, lis le TL;DR + mental model, fais un exercice, reviens. La compréhension du runtime se construit par les mains, pas par la lecture.
Niveau 1 — Runtime fundamentals
Le socle. Tout ce qui suit suppose que tu sais ce qu'est l'event loop et un stream. Si tu ne devais lire que deux choses du hub : §1 et §4.
- 01 — Event loop, libuv, micro/macro tasks
- 02 — Process model & worker threads
- 03 — Modules : CJS vs ESM, dual packaging
- 04 — Streams (Readable/Writable/Transform)
- 05 — Buffer, encodings, binary
Niveau 2 — I/O & Networking
Node est un runtime d'I/O avant tout. Ici tu apprends à parler HTTP/2, à appeler des services avec le fetch natif (timeout + AbortController obligatoires), et à manipuler le filesystem sans bloquer.
- 01 — HTTP server, HTTP/2, HTTP/3
- 02 — fetch natif, AbortController, timeouts
- 03 — Filesystem (fs.promises, watch)
- 04 — TCP/UDP/Sockets
Niveau 3 — Concurrence & Perf
Le niveau qui sépare le dev du senior. Quand mettre du worker_threads vs cluster vs child_process, comment contrôler la concurrence (et pas juste lancer 10 000 promesses), comment profiler une latence et chasser une fuite mémoire à coups de heap snapshots.
- 01 — Worker threads vs cluster vs child_process
- 02 — Async patterns (concurrency primitives)
- 03 — Profiling (clinic.js, 0x, --inspect)
- 04 — Memory : leaks, GC, heap snapshots
Niveau 4 — Frameworks & APIs
Du classique (Express) au moderne (Fastify, Hono edge-ready) jusqu'au type-safe de bout en bout (tRPC + Zod). On compare les modèles, pas juste les API.
Niveau 5 — Qualité
Tester avec le runner natif (node:test), faire du TypeScript sans build (--experimental-strip-types), linter vite (Biome), choisir son package manager en connaissance de cause.
- 01 — Testing (native runner, vitest, jest)
- 02 — TypeScript (tsc / tsx / swc / Node 22 --strip-types)
- 03 — Linting (Biome vs ESLint)
- 04 — Package managers (pnpm / bun / npm / yarn)
Niveau 6 — Production
Là où le code rencontre la réalité : traces et logs structurés (OpenTelemetry, pino), sécurité et supply chain, conteneurisation avec signaux et graceful shutdown, et le panorama des versions LTS.
- 01 — Observabilité (OpenTelemetry, pino)
- 02 — Sécurité (helmet, audit, supply chain)
- 03 — Déploiement Docker, signals, graceful shutdown
- 04 — Versions 18 → 24 (LTS, nouveautés)
📌 Cheat sheet versions 18 → 24
Repère mental rapide. Détail complet dans 06 — Versions 18 → 24.
| Version | Statut (mi-2026) | À retenir |
|---|---|---|
| 18 | LTS, fin de support avril 2025 → à migrer | fetch natif (expérimental), node:test, --watch |
| 20 | LTS Active jusqu'à 2026 | fetch stable, permission model (--experimental-permission), node:test stable |
| 22 | LTS « Jod » (recommandée prod) | --experimental-strip-types (TS sans build), require(esm), WebSocket client natif, V8 12.x |
| 24 | Current → LTS oct. 2026 | --strip-types activé par défaut, npm 11, V8 13.x, URLPattern global, perf I/O |
Choix par défaut en 2026 : déploie sur 22 LTS (stable, support long), suis la 24 en CI pour anticiper. Ne démarre aucun nouveau projet sur 18.
# Épingle la version par projet (lu par nvm, fnm, Volta, et les CI)
echo "22" > .nvmrc
node --version # vérifie
node --test # runner natif, zéro dépendance
node --run build # Node 22+ : lance les scripts package.json sans npm🏗️ Comment un staff engineer lit une stack Node
Au-delà des leçons, voici la grille de lecture qui distingue un senior. Devant un incident ou une revue d'archi :
- « C'est lent » n'existe pas. Lent = CPU-bound (event loop bloqué → profile avec
--prof/ clinic flame) ou I/O-bound (en attente réseau/DB → trace distribuée) ou event-loop lag (trop de microtasks, GC agressif →monitorEventLoopDelay). Trois diagnostics, trois remèdes. Confondre les trois = perdre une journée. - Un seul process ne scale pas verticalement sur le CPU. Pour saturer une machine N cœurs sur du CPU-bound, il faut N process (
cluster, ou N pods derrière un load balancer) ou desworker_threads. Un process Node ne « grossit » pas tout seul. - La résilience se code dans les I/O sortantes. Chaque appel réseau a un
timeout+AbortController+ retry avec backoff + circuit breaker. Sans ça, une dépendance lente devient ta propre panne (cascade). - La prod commence par l'arrêt. Un service qui ne fait pas de graceful shutdown (SIGTERM → stop accepting → drain in-flight → close DB → exit) perd des requêtes à chaque deploy. C'est le premier test d'un déploiement sérieux.
- La supply chain est ta surface d'attaque n°1.
npm audit, lockfile commité,--ignore-scriptsau CI, et tu sais ce qu'unpostinstallpeut faire à ta machine.
🏋️ Exercices (transverses au hub)
Ces exercices traversent plusieurs niveaux : ils te forcent à connecter runtime, I/O, concurrence et prod. Fais-les dans l'ordre, chacun monte d'un cran.
1. Prouver le modèle d'exécution
Objectif : démontrer empiriquement l'ordre sync → nextTick → Promise → timers/immediate.
Écris un script qui logge dans cet ordre attendu, en mélangeant console.log direct, process.nextTick, Promise.resolve().then, setTimeout(…, 0) et setImmediate. Prédis la sortie avant de lancer, puis explique chaque écart.
Indice/Solution : sync d'abord, puis toutes les
nextTick, puis toutes les microtasks Promise, puis la phase timers (setTimeout) ou check (setImmediate) selon le contexte. Dans un callback I/O,setImmediatepasse toujours avantsetTimeout. Détails : 01 — Event loop.
2. Le serveur qui ne bloque jamais
Objectif : servir une route HTTP qui calcule un hash coûteux sans geler les autres requêtes.
Crée un serveur HTTP natif avec deux routes : /fast (répond ok instantanément) et /heavy (calcule un pbkdf2 à 1M itérations). Version naïve synchrone, puis prouve avec un autocannon -c 50 que /fast s'effondre. Corrige en déplaçant le calcul vers crypto.pbkdf2 (async, threadpool) puis vers un worker_threads. Mesure le p99 de /fast dans les trois cas.
Indice/Solution : version sync →
/fastbloqué pendant chaque hash. Versioncrypto.pbkdf2async → mieux, mais le threadpool (4 threads) sature à 5+ requêtes simultanées (UV_THREADPOOL_SIZE!). Worker pool →/fastreste plat. Voir Workers vs cluster.
3. Streaming sans exploser la RAM
Objectif : transformer un fichier de 5 Go ligne par ligne en restant sous 100 Mo de heap.
Lis un gros fichier, applique une transformation (uppercase, ou compte des occurrences), écris le résultat — le tout en streaming avec pipeline(). Compare la RAM (process.memoryUsage().heapUsed) avec la version readFile/writeFile naïve. Génère le fichier toi-même.
Indice/Solution :
readFilecharge tout en RAM → OOM.pipeline(readStream, transformStream, writeStream)applique la backpressure automatiquement : leReadableralentit quand leWritableest saturé.pipeline(vs.pipe()) propage les erreurs et nettoie les handles. Voir Streams.
4. Concurrence bornée (production-grade)
Objectif : appeler 10 000 URLs avec au plus 20 requêtes en vol, timeout par requête, et retry.
N'utilise pas Promise.all(urls.map(fetch)) (tu vas DDoS la cible et toi-même). Implémente un pool de concurrence borné : 20 workers qui pompent une file. Chaque fetch a un AbortController à 3 s et 2 retries en backoff exponentiel. Agrège succès/échecs.
Indice/Solution : worker pool avec un index partagé/générateur async, ou
p-limit-like maison.AbortSignal.timeout(3000)pour le timeout natif. Retry :await sleep(2 ** attempt * 100 + jitter). Voir fetch & AbortController et Async patterns.
5. Graceful shutdown (break-then-fix)
Objectif : zéro requête perdue lors d'un redéploiement.
Lance ton serveur de l'exercice 2 sous charge (autocannon), envoie-lui un SIGTERM (kill), et compte les requêtes échouées. Constate les drops. Implémente : server.close() (stop accepting), attente du drain des requêtes en cours avec un timeout dur, fermeture des connexions DB/keep-alive, process.exit(0). Re-mesure : zéro drop.
Indice/Solution :
process.on('SIGTERM', …),server.close(cb)ne ferme pas les connexions keep-alive ouvertes → il faut tracker les sockets ou utiliserserver.closeIdleConnections()/closeAllConnections()(Node 18.2+) après un délai. Timeout de sécurité (setTimeout(forceExit, 10_000).unref()). Voir Déploiement & signals.
6. Chasse à la fuite mémoire (expert)
Objectif : trouver et corriger une fuite dans un service qui grossit lentement.
Écris volontairement un service qui fuit (ex : un Map global qui accumule, ou un EventEmitter avec des listeners jamais retirés → MaxListenersExceededWarning). Observe heapUsed croître. Prends deux heap snapshots à 5 min d'intervalle, compare le delta dans Chrome DevTools (--inspect), identifie le retainer, corrige.
Indice/Solution : snapshot 1, charge, snapshot 2, vue « Comparison » triée par delta d'objets. Le retainer pointe vers ta
Map/tes listeners. Fixes classiques :WeakMap/WeakRefpour les caches,removeListener/AbortControllerpour les listeners, bornage LRU. Voir Memory, GC, heap snapshots.
🎤 En entretien
Questions transverses qu'on pose à un candidat senior Node. Les réponses courtes ici ; le « pourquoi » détaillé est dans chaque leçon.
« Node est-il mono-thread ? » Le thread d'exécution JS (V8) l'est : ton code tourne sur un seul thread. Mais le runtime est multi-thread — libuv a un threadpool (4 par défaut) pour fs/crypto/dns/zlib, le réseau passe par le kernel async, et tu peux ajouter des
worker_threads. Donc : non, le process n'est pas mono-thread, mais ton code applicatif s'exécute séquentiellement. La vraie compétence : savoir ce qui quitte le thread JS et ce qui le bloque.« Différence entre
process.nextTick,setImmediateetsetTimeout(fn, 0)? »nextTickse vide avant toute autre microtask et avant de quitter la phase courante → risque de famine si récursif. Les Promises (.then/queueMicrotask) se vident juste après lesnextTick.setImmediates'exécute en phase check (aprèspoll),setTimeout(0)en phase timers. Dans un callback I/O,setImmediategagne toujours contresetTimeout(0).« Comment scaler un service Node CPU-bound ? » D'abord identifier que c'est bien CPU-bound (event loop lag, flame graph). Un seul process ne suffit pas : soit
cluster/ N pods (un process par cœur, partage du port via le master ou le LB) pour des requêtes indépendantes, soitworker_threads+ pool pour décharger des tâches lourdes ponctuelles tout en gardant le thread principal réactif.worker_threadspartage de la mémoire (SharedArrayBuffer) mais isole les V8 ;clusterisole tout. Ne jamais simplement « ajouter de l'async » : l'async ne crée pas de parallélisme CPU.« Que fais-tu pour qu'un déploiement ne perde aucune requête ? » Graceful shutdown sur
SIGTERM: arrêter d'accepter (server.close), drainer les requêtes en vol avec un timeout dur, fermer proprement DB/pools/keep-alive (closeAllConnectionsaprès délai), puisexit(0). Côté infra : readiness probe qui passe à not ready avant l'arrêt,preStophook,terminationGracePeriodSecondscohérent avec ton timeout. Bonus senior :unref()sur le timer de force-exit pour ne pas garder le process en vie inutilement.