Mémo Node.js

JavaScript côté serveur. Runtime asynchrone orienté événements.

Qu'est-ce que Node.js ?

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).

Les piliers

  • 🚀 V8 : Vitesse d'exécution quasi-native
  • 🧵 Asynchrone : Rien n'est bloquant (par défaut)
  • 📦 NPM : Le plus grand écosystème au monde
  • 🔄 JSON : Langage natif de bout en bout

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.

01

Runtime & Async

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.

Architecture Node.js

Node.js combine le moteur V8 (Chrome) avec Libuv.

  • V8 Engine : Compile et exécute le JS.
  • Libuv : Gère l'Event Loop, les Thread Pool (pour I/O lourds) et les événements système.
  • Single Threaded : Un seul stack d'exécution JS (Main Thread).
⚠️ Don't Block the Loop !
Le JSON.parse d'un fichier de 100MB ou une boucle `while(true)` bloquera TOUS les utilisateurs. Pour du calcul lourd, utilisez les Worker Threads.
💡 Deep Dive: L'Event Loop

L'Event Loop orchestre tout. Elle tourne en boucle et passe par différentes Phases :

┌───────────────────────────┐
│ Timers (setTimeout) │ ◀── Le callback est exécuté ici
└─────────────┬─────────────┘

┌─────────────▼─────────────┐
│ Pending Callbacks │ ◀── Erreurs système (OS)
└─────────────┬─────────────┘

┌─────────────▼─────────────┐
│ Idle, Prepare │ ◀── Usage interne
└─────────────┬─────────────┘

┌─────────────▼─────────────┐
│ POLL │ ◀── 🚨 LE COEUR : I/O, FS, Network
└─────────────┬─────────────┘

┌─────────────▼─────────────┐
│ Check (setImmediate) │ ◀── Exécuté juste après Poll
└─────────────┬─────────────┘

┌─────────────▼─────────────┐
│ Close Callbacks │ ◀── socket.on('close', ...)
└───────────────────────────┘

Microtasks vs Macrotasks

Entre chaque phase, Node.js vide DEUX queues prioritaires :

  1. process.nextTick() : Priorité ABSOLUE. S'exécute immédiatement après le code courant, avant même les promesses. ⚠️ Danger de boucle infinie.
  2. Promise (Microtasks) : S'exécute après nextTick, avant de passer à la phase suivante de l'Event Loop.
Priorité d'exécution (Piège Classique)
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
*/

Worker Threads

Pour les tâches CPU-Intensive (crypto, compression, traitement image).

import { Worker } from 'node:worker_threads';

Cluster Mode

Pour scaler sur plusieurs coeurs CPU (via PM2 ou module natif).

Simulateur Event Loop

Visualisez l'ordre d'exécution réel entre les différentes queues.

En attente...
02

Modules

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.

💡 Le Singleton Un module n'est exécuté qu'une seule fois. Le résultat est mis en cache. Au prochain `import`/`require`, Node retourne l'objet cached. Si vous modifiez l'objet exporté, tous les autres fichiers verront la modification (Mutable).
🔍 CommonJS (CJS) - Under the hood

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).

CommonJS (Legacy)
// export
module.exports = { hello };

// import
const lib = require('./lib');

// Dynamic Import (Possible)
if (cond) require('./opt');
ES Modules (Standard)
// export
export default function() {}
export const val = 42;

// import
import fn, { val } from './lib.mjs';

// Top-level await (Cool!)
const data = await fetch(...);

ESM : Les pièges classiques

❌ __dirname n'existe pas

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);

⚠️ Extensions obligatoires

En ESM natif, l'import relatif DOIT avoir l'extension.

// BAD
import utils from './utils';

// GOOD
import utils from './utils.js';
🔄 Cycles & Dépendances Circulaires

Si A importe B, et B importe A...

  • CJS : Retourne un objet export partiel (vide ou incomplet) pour casser la boucle. Peut causer des crashs.
  • ESM : Gère mieux les références (Binding live), mais reste dangereux.

Solution : Refactoriser. Extraire la partie commune dans un Module C.

03

File System (fs)

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.

Le module Path (Vital)
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

Les 3 modes d'accès

  • Sync (readFileSync) : Bloque tout le thread. Uniquement pour charger une config au démarrage.
  • Callback (readFile) : Legacy, rapide, mais verbeux.
  • Promise (fs/promises) : Le standard moderne. Compatible async/await.

Streaming (Big Files)

Pour les gros fichiers (ex: Vidéo, Log de 1GB), ne jamais tout charger en RAM !

Lecture Streamée
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é'));

Dossiers & Arborescence

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);

Watch Mode

Surveiller les changements de fichiers.

fs.watch('config.json', (evt, file) => reload())
Virtual File System
En attente d'opération...
04

Events

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é).

⚠️ Gestion des Fuites Mémoire (MaxListeners)

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
Méthodes clés
// 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);
🚨 L'événement 'error'
// 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)
Event Simulation
Log des événements...
05

HTTP (Natif)

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).

Serveur Raw (Sans Framework)
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);

HTTPS / SSL

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.

06

Express.js

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.

⚙️ Les Middlewares : La chaîne de production

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.

Chain of Responsibility
// 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');
});

Features Essentielles

📁 Fichiers Statiques

Servir CSS, Images, JS du frontend.

app.use(express.static('public'));

🔀 Le Router (Modularité)

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);
Template Engines (Server-Side Rendering)
Bien que Node soit souvent utilisé pour des API JSON (avec React/Vue en front), on peut générer du HTML avec EJS, Pug ou Handlebars.
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
    res.render('index', { title: 'Mon Site' });
});
07

NPM & Packages

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 vs NPM

  • NPM : Installe les paquets (local ou global).
  • NPX : Exécute un binaire sans l'installer globalement (ex: npx create-react-app).

Semantic Versioning (SemVer)

MAJOR.MINOR.PATCH

  • ^1.2.3 : Update Minor & Patch (Safe)
  • ~1.2.3 : Update Patch only (Very Safe)

Scripts & Lifecycle

package.json
"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js",
  "test": "jest",
  "build": "tsc",
  "prebuild": "rimraf dist" // S'exécute AVANT build
}
🔒 Package-lock.json
Ce fichier est CRUCIAL. Il verrouille l'arbre exact des dépendances. DOIT être commité. Il garantit que "ça marche chez moi" = "ça marche en prod".
Commandes Avancées
# 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

Alternatives

Yarn ou PNPM (plus rapide, gestion disque optimisée) sont très populaires en entreprise.

08

Configurations

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.

⚠️ Sécurité & Git
Ajoutez TOUJOURS .env à votre .gitignore.
Commitez un fichier .env.example avec les clés vides pour l'équipe.
Utilisation Standard
// 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 !");
}

Typage & Validation

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);

Cross-Env

Windows et Linux/Mac gèrent les variables différemment dans le terminal.

Solution (package.json)
// 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"
09

Streams

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.

Les 4 Types de Streams

  • Readable : Source (fs.read, req). On lit.
  • Writable : Destination (fs.write, res). On écrit.
  • Duplex : Les deux (Socket TCP).
  • Transform : Modifie les données à la volée (Gzip, Crypto).

🌊 Backpressure

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.

Modern Streams (Pipeline)

Pipeline Async (Best Practice)
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 !
}

Generators & Streams

On peut transformer un Stream en async iterator.

for await (const chunk of stream) {
    process(chunk);
}
10

Gestion d'Erreurs

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.

Custom Error Class
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);
🚨 Erreurs Fatales
Certaines erreurs ne peuvent pas être capturées par un `try/catch` (ex: syntaxe, manque de mémoire). Pour le reste, utilisez :
  • process.on('uncaughtException')
  • process.on('unhandledRejection')

Best Practices : Async/Await

Le pattern "Catch-All" Express
// 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);
}));
💡 Operational vs Programmer Errors
- Operational : On s'y attend (ex: timeout, 404). On doit les gérer proprement.
- Programmer : Bugs (ex: undefined is not a function). Solution : CRASH & RESTART (via PM2). Ne jamais essayer de continuer une app corrompue.
11

Process & OS

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.

Child Processes (L'art de déléguer)
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));

Arguments & CWD

Accéder aux arguments passés en console :

process.argv[2]

Dossier actuel :

process.cwd()

Graceful Shutdown (Standard de Production)

Indispensable en Docker pour ne pas couper les requêtes HTTP en cours lors d'un déploiement.

SIGINT / SIGTERM
process.on('SIGTERM', () => {
  console.log('Ressu SIGTERM. Fermeture propre...');
  server.close(() => {
    console.log('Serveur fermé. Fin du process.');
    db.disconnect();
    process.exit(0);
  });
});

os module

  • os.uptime() : Temps de marche
  • os.loadavg() : Charge CPU
  • os.networkInterfaces() : IPs locales
99

Quiz Expert

Testez vos connaissances sur Node.js

Chargement du quiz...

🎓 Conseils pour l'examen

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.