Skip to content

🟢 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 :

  1. Est-ce que ça bloque le thread JS ? Une boucle CPU, un JSON.parse de 50 Mo, un fs.readFileSync, un bcrypt synchrone → toute l'app gèle. Mesure le lag avec monitorEventLoopDelay.
  2. 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.
  3. 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 parPuisNe saute pas
Dev JS qui découvre le backend NodeNiveau 1 (event loop, modules)Niveau 2 (HTTP, fetch)Streams (Niv. 1) — c'est le concept le plus sous-estimé
Dev Express confirmé qui vise seniorNiveau 1 §1 + §4, Niveau 3 entierNiveau 6 (prod)Memory/GC & profiling (Niv. 3)
Migration vers Node récentNiveau 6 §4 (versions 18→24)Niveau 5 §2 (TS natif)ESM/dual packaging (Niv. 1 §3)
Prépa entretien seniorLes blocs 🎤 En entretien de chaque leçonNiveau 3 entierEvent 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.

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.

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.

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.

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.


📌 Cheat sheet versions 18 → 24

Repère mental rapide. Détail complet dans 06 — Versions 18 → 24.

VersionStatut (mi-2026)À retenir
18LTS, fin de support avril 2025 → à migrerfetch natif (expérimental), node:test, --watch
20LTS Active jusqu'à 2026fetch stable, permission model (--experimental-permission), node:test stable
22LTS « Jod » (recommandée prod)--experimental-strip-types (TS sans build), require(esm), WebSocket client natif, V8 12.x
24Current → 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.

bash
# É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 des worker_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-scripts au CI, et tu sais ce qu'un postinstall peut 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, setImmediate passe toujours avant setTimeout. 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 → /fast bloqué pendant chaque hash. Version crypto.pbkdf2 async → mieux, mais le threadpool (4 threads) sature à 5+ requêtes simultanées (UV_THREADPOOL_SIZE !). Worker pool → /fast reste 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 : readFile charge tout en RAM → OOM. pipeline(readStream, transformStream, writeStream) applique la backpressure automatiquement : le Readable ralentit quand le Writable est 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 utiliser server.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/WeakRef pour les caches, removeListener/AbortController pour 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, setImmediate et setTimeout(fn, 0) ? » nextTick se 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 les nextTick. setImmediate s'exécute en phase check (après poll), setTimeout(0) en phase timers. Dans un callback I/O, setImmediate gagne toujours contre setTimeout(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, soit worker_threads + pool pour décharger des tâches lourdes ponctuelles tout en gardant le thread principal réactif. worker_threads partage de la mémoire (SharedArrayBuffer) mais isole les V8 ; cluster isole 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 (closeAllConnections après délai), puis exit(0). Côté infra : readiness probe qui passe à not ready avant l'arrêt, preStop hook, terminationGracePeriodSeconds cohérent avec ton timeout. Bonus senior : unref() sur le timer de force-exit pour ne pas garder le process en vie inutilement.

Bibliothèque tech perso — Achref