JavaScript côté serveur. Runtime asynchrone orienté événements.
Node.js est un Runtime JavaScript bâti sur le moteur V8 de Chrome. Il permet de porter la puissance du Web côté serveur avec une approche unifiée.
Créé par Ryan Dahl en 2009, il a révolutionné le backend en prouvant qu'un modèle asynchrone basé sur les événements peut surpasser les modèles multi-threadés classiques pour les entrées/sorties (I/O).
Mise en situation : Contrairement à JavaScript dans le navigateur qui manipule le DOM visuel, Node.js gère l'envers du décor. Il écoute les requêtes du monde entier, interagit avec les bases de données (MongoDB, PostgreSQL) et renvoie les données (JSON) au client. Il est le cœur d'une architecture Fullstack moderne.
Event Loop, Libuv & Architecture Non-Bloquante
Mise en situation : Imaginez un restaurant très fréquenté. Au lieu d'avoir un serveur par table qui attend (bloqué) que le client lise la carte (modèle classique Thread-par-Requête), Node.js fonctionne comme un seul serveur super-rapide (Single Thread). Il prend la commande, l'envoie en cuisine (Thread Pool C++), et passe immédiatement à la table suivante. Dès qu'un plat est prêt, la cuisine prévient le serveur qui le sert via la fameuse Event Loop.
Node.js combine le moteur V8 (Chrome) avec Libuv.
L'Event Loop orchestre tout. Elle tourne en boucle et passe par différentes Phases :
Entre chaque phase, Node.js vide DEUX queues prioritaires :
console.log("1. Start"); setTimeout(() => console.log("5. Timeout (Macrotask)"), 0); setImmediate(() => console.log("6. Check Phase")); new Promise(resolve => { console.log("2. Promise Init (Sync)"); resolve(); }).then(() => console.log("4. Promise .then (Microtask)")); process.nextTick(() => console.log("3. NextTick (Top Priority)")); console.log("End Stack"); /* Ordre Réel : 1. Start 2. Promise Init End Stack 3. NextTick 4. Promise .then 5. Timeout 6. Check Phase */
Pour les tâches CPU-Intensive (crypto, compression, traitement image).
import { Worker } from 'node:worker_threads';
Pour scaler sur plusieurs coeurs CPU (via PM2 ou module natif).
Visualisez l'ordre d'exécution réel entre les différentes queues.
CommonJS, ESM & Internals
Mise en situation : Dans Node.js, chaque fichier est un monde clos (module). Pour partager des fonctions entre fichiers, on utilisait historiquement require (CommonJS, l'héritage de Node), mais le standard universel du web est désormais import/export (ES Modules). Voici comment l'information voyage d'un fichier à l'autre de manière structurée.
Quand Node exécute un fichier JS en CJS, il ne l'exécute pas "nu". Il l'enveloppe dans une fonction (IIFE) :
(function(exports, require, module, __filename, __dirname) {
// Votre code vit ici
});
C'est pour cela que __dirname, exports, etc. sont disponibles
"globalement"
(mais en fait locaux à la fonction).
// export module.exports = { hello }; // import const lib = require('./lib'); // Dynamic Import (Possible) if (cond) require('./opt');
// export export default function() {} export const val = 42; // import import fn, { val } from './lib.mjs'; // Top-level await (Cool!) const data = await fetch(...);
En ESM, `__dirname` n'est pas défini.
// Remplacement : import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
En ESM natif, l'import relatif DOIT avoir l'extension.
// BAD import utils from './utils'; // GOOD import utils from './utils.js';
Si A importe B, et B importe A...
Solution : Refactoriser. Extraire la partie commune dans un Module C.
Manipulation de fichiers & Chemins
Mise en situation : Lire un fichier vidéo de 2 Go d'un coup avec readFileSync fige votre serveur et sature votre RAM (c'est comme essayer d'avaler un gâteau entier d'un coup). La méthode moderne avec les Streams (createReadStream) lit le fichier morceau par morceau (chunks), gardant le serveur fluide et la mémoire vive (RAM) intacte.
import path from 'node:path'; // ⚠️ NE JAMAIS faire 'dir/file.txt' (Windows utilise \) // Toujours utiliser path.join() const filePath = path.join('data', 'logs', 'error.log'); // -> "data/logs/error.log" (Linux) // -> "data\logs\error.log" (Windows) const abs = path.resolve('config.json'); // -> Chemin absolu depuis racine process
readFileSync) : Bloque tout le
thread.
Uniquement pour charger une config au démarrage.readFile) : Legacy,
rapide,
mais verbeux.fs/promises) : Le
standard moderne. Compatible async/await.Pour les gros fichiers (ex: Vidéo, Log de 1GB), ne jamais tout charger en RAM !
import fs from 'node:fs'; const stream = fs.createReadStream('giant-video.mp4', { highWaterMark: 64 * 1024 // Lire par chunk de 64KB }); stream.on('data', (chunk) => { console.log(`Reçu ${chunk.length} bytes`); // Envoyer au client au fur et à mesure... }); stream.on('end', () => console.log('Terminé'));
Pour lister efficacement des dossiers géants, utilisez opendir (Itérateur) plutôt
que
readdir (Tableau complet).
const dir = await fs.opendir('./img'); for await (const dirent of dir) console.log(dirent.name);
Surveiller les changements de fichiers.
fs.watch('config.json', (evt, file) => reload())
Architecture et Design Patterns
Mise en situation : Imaginez une station radio FM (l'Emitter) et ses auditeurs (les Listeners). Quand l'Emitter diffuse la météo, tous les récepteurs allumés réagissent en même temps mais indépendamment. C'est l'Architecture Orientée Événements (Pub/Sub) : un découplage parfait où l'émetteur (ex: la base de données) ne sait même pas qui l'écoute (ex: l'envoi d'email, les logs de sécurité).
Si vous attachez trop de listeners à un même événement (plus de 10 par défaut), Node.js vous avertira :
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
Pourquoi ? Si vous ajoutez un listener dans une boucle ou une requête sans le retirer, ils s'accumulent indéfiniment.
myEmitter.setMaxListeners(20); // Augmenter la limite
// 1. on() : Écoute permanente emitter.on('data', cb); // 2. once() : Écoute UNE SEULE fois emitter.once('init', () => console.log('Démarré')); // 3. off() / removeListener() : Arrêter const log = (msg) => console.log(msg); emitter.on('msg', log); emitter.off('msg', log);
// Si 'error' est émis SANS listener... // Node CRASH immédiatement ! myEmitter.on('error', (err) => { console.error('Erreur gérée :', err); }); myEmitter.emit('error', new Error('Oups')); // -> "Erreur gérée : Oups" // (Le process continue)
Bas niveau & Anatomie des requêtes
Mise en situation : Quand vous naviguez sur internet, votre navigateur (le Client) expédie un colis textuel de demande (une Requête HTTP) vers l'ordinateur distant. Votre application Node.js (le Serveur HTTP) intercepte ce colis sur son port réseau (ex: port 80), l'analyse, puis expédie un colis textuel de retour (une Réponse HTTP).
import http from 'node:http'; http.createServer((req, res) => { // 1. Routing manuel (verbe + url) if (req.method === 'POST' && req.url === '/echo') { // 2. Lire le Body (Stream) let body = []; req.on('data', chunk => body.push(chunk)); req.on('end', () => { body = Buffer.concat(body).toString(); // 3. Envoyer Réponse (Headers + Data) res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ echo: body })); }); } else { res.statusCode = 404; res.end(); } }).listen(3000);
Pour activer HTTPS, il faut charger les certificats (clé privée et cert public).
import https from 'node:https'; import fs from 'node:fs'; const options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }; https.createServer(options, app).listen(443);
En prod, c'est souvent le Reverse Proxy (Nginx) qui gère le SSL, pas Node directement.
Middlewares, Routing & Static Files
Mise en situation : Créer un serveur web en gérant nativement HTTP, c'est comme concevoir une voiture pièce par pièce ; fonctionnel mais chronophage. Express.js vous offre l'usine d'assemblage préconfigurée. Son immense pouvoir vient des Middlewares : un grand entonnoir composé de filtres successifs (sécurité, formatage de données, logs) par lequel passe chaque requête HTTP avant d'arriver au poste final.
Tout est middleware dans Express. C'est une fonction qui a accès à req,
res, et next.
Si vous n'appelez pas next() ou si vous ne répondez pas (res.send), la
requête pend indéfiniment.
// Middleware 1 : Logger app.use((req, res, next) => { console.log(`Request: ${req.url}`); next(); // ➡️ Passe au suivant }); // Middleware 2 : Auth Guard app.use('/admin', (req, res, next) => { if (!req.auth) return res.sendStatus(403); next(); // ➡️ Passe au controller }); // Route Handler (Final Middleware) app.get('/admin', (req, res) => { res.send('Welcome Admin'); });
Servir CSS, Images, JS du frontend.
app.use(express.static('public'));
Pour ne pas avoir un fichier de 5000 lignes.
// users.js const router = express.Router(); router.get('/', getAll); export default router; // app.js app.use('/users', userRouter);
app.set('view engine', 'ejs'); app.get('/', (req, res) => { res.render('index', { title: 'Mon Site' }); });
Gestionnaire de paquets, Scripts & Versioning
Mise en situation : Pourquoi réinventer la roue pour compresser un fichier ZIP ou valider un email ? NPM (Node Package Manager) est le plus grand catalogue de briques logicielles open source au monde. Il installe les outils des autres développeurs et gère automatiquement l'enfer des dépendances (l'arborescence tentaculaire du fameux dossier node_modules) pour que vous restiez concentré sur votre métier.
npx create-react-app).
MAJOR.MINOR.PATCH
"scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest", "build": "tsc", "prebuild": "rimraf dist" // S'exécute AVANT build }
# Auditer les vulnérabilités npm audit # Mettre à jour les paquets (selon package.json) npm update # Installer version précise npm i express@4.17.1 # Prune (supprimer packages inutiles) npm prune
Yarn ou PNPM (plus rapide, gestion disque optimisée) sont très populaires en entreprise.
Variables d'Environnement (12-Factor App)
Mise en situation : Une application web est souvent testée sur votre PC (Local), puis sur un serveur de pré-production (Staging), et enfin pour le public (Production). Le fichier de configuration .env permet d'injecter des données différentes selon le serveur (ex: le mot de passe de la base de données) sans avoir à écrire de "Si (production) alors..." dans le code. C'est le principe central des variables d'environnement.
.env à votre .gitignore..env.example avec les clés vides pour l'équipe.
// npm i dotenv import 'dotenv/config'; const PORT = process.env.PORT || 3000; const DB_URL = process.env.DB_URL; if (!DB_URL) { throw new Error("DB_URL manquante !"); }
En production, il est vital de valider les variables au démarrage. Utilisez Zod, Joi ou Envalid.
import { z } from 'zod'; const envSchema = z.object({ PORT: z.coerce.number(), API_KEY: z.string().min(10) }); export const env = envSchema.parse(process.env);
Windows et Linux/Mac gèrent les variables différemment dans le terminal.
// Sans cross-env (Bug sur Windows) "start": "NODE_ENV=production node app.js" // Avec cross-env (Universel) "start": "cross-env NODE_ENV=production node app.js"
Flux de données, Backpressure & Pipeline
Mise en situation : Imaginez devoir télécharger et afficher un film de 10 Go sur un serveur qui n'a que 2 Go de mémoire vive. Impossible de tout charger d'un coup ! Les Streams (flux) résolvent ce problème en découpant et en envoyant les données petit à petit (par petits paquets de RAM), comme l'eau s'écoulant d'un robinet, évitant la saturation de votre application.
Si le disque écrit moins vite que le réseau ne lit, la mémoire explose.
Le Pipe gère automatiquement la "Backpressure" : il dit à la source de pauser (pause/resume) le temps que la destination rattrape.
import { pipeline } from 'node:stream/promises'; import fs from 'node:fs'; import zlib from 'node:zlib'; async function archiveLog() { await pipeline( fs.createReadStream('app.log'), // 1. Lire zlib.createGzip(), // 2. Compresser fs.createWriteStream('app.log.gz') // 3. Écrire ); console.log('Succès !'); // Pipeline gère aussi la fermeture auto des streams et les erreurs ! }
On peut transformer un Stream en async iterator.
for await (const chunk of stream) { process(chunk); }
Robustesse, Custom Errors & Crash Prevention
Mise en situation : En programmation, la question n'est pas si une erreur va arriver, mais quand. Que faire si la base de données ne répond plus ou si le client envoie des données corrompues ? Une bonne gestion d'erreur (Try/Catch) intercepte la bombe avant qu'elle n'explose, informe l'utilisateur proprement, et maintient l'application en vie.
class ApiError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } throw new ApiError("Produit non trouvé", 404);
process.on('uncaughtException')process.on('unhandledRejection')// Un wrapper pour éviter les try/catch répétitifs const catchAsync = fn => (req, res, next) => { fn(req, res, next).catch(next); }; app.get('/data', catchAsync(async (req, res) => { const result = await heavyTask(); res.json(result); }));
Gestion du système & Multi-processus
Mise en situation : Node.js est un programme comme un autre qui s'exécute sur le système d'exploitation de votre serveur (Linux, Windows). Via le module process et la création d'« enfants » (child_process), Node.js devient un chef d'orchestre capable de dialoguer directement avec le système : lire les performances CPU, déléguer des encodages lourds à d'autres programmes, et orchestrer son propre arrêt de manière élégante.
import { spawn, exec } from 'node:child_process'; // exec : Petit script (Bufferise tout la sortie) exec('ls -lh', (err, stdout) => { console.log(stdout); }); // spawn : Flux de données (Longues tâches) const ls = spawn('find', ['/']); ls.stdout.on('data', data => console.log(data));
Accéder aux arguments passés en console :
process.argv[2]
Dossier actuel :
process.cwd()
Indispensable en Docker pour ne pas couper les requêtes HTTP en cours lors d'un déploiement.
process.on('SIGTERM', () => { console.log('Ressu SIGTERM. Fermeture propre...'); server.close(() => { console.log('Serveur fermé. Fin du process.'); db.disconnect(); process.exit(0); }); });
os.uptime() : Temps de marcheos.loadavg() : Charge CPUos.networkInterfaces() : IPs localesTestez vos connaissances sur Node.js
Ce quiz couvre des notions allant du débutant à l'expert. Si vous obtenez plus de 80%, vous maîtrisez les concepts fondamentaux de l'architecture Node.js.