⚡️ Node.js – Installation, Core & Déploiement
Guide complet IDEO‑Lab sur l'runtime JavaScript (Event Loop, NPM, Async, Express, PM2).
Vue d'ensemble
Runtime JS (V8), I/O non-bloquant, backend.
V8 BackendEvent Loop (Cœur)
Boucle d'événements, non-bloquant, single-thread.
Event Loop Non-blockingInstallation (NVM)
nvm (Node Version Manager) (reco), LTS.
NPM & package.json
npm init, install, scripts, dependencies.
Modules (CJS vs ESM)
require (CommonJS) vs import (ESM).
Core : fs & path
File System (async/sync), gestion des chemins.
fs pathCore : http & events
Serveur HTTP (natif), EventEmitter.
http eventsCore : Buffers & Streams
Données binaires (Buffer), .pipe() (Streams).
Callbacks
Le style "Callback Hell" (pyramide).
Callback Callback HellPromises (Promesses)
.then(), .catch(), Promise.all().
Async / Await
Syntaxe moderne (async function, await, try/catch).
Express.js (Intro)
Installation, Hello World, app.listen.
Express (Routing)
app.get, app.post, req.params, req.body.
Express (Middleware)
app.use(), express.json(), CORS, logger.
Base de Données (Postgres)
npm install pg, Pool de connexions, pool.query.
API Client (Axios)
npm install axios, axios.get, axios.post.
Process Manager (PM2)
pm2 start, clustering, --watch, logs, restart.
Déploiement (Nginx)
Reverse proxy Nginx vers PM2 (localhost).
Nginx proxy_passCheat-sheet Node
Commandes NPM, NVM, Core modules.
cheat npmRuntime JavaScript (Backend)
Node.js est un **runtime** (environnement d'exécution) JavaScript open-source, multiplateforme, qui permet d'exécuter du JavaScript **côté serveur** (backend).
Créé par Ryan Dahl en 2009, il prend le moteur JavaScript V8 (le cœur de Google Chrome) et l'intègre dans un programme C++, en y ajoutant des modules pour le système (fichiers, réseau...).
Node.js n'est PAS un framework. C'est l'environnement sur lequel tournent les frameworks (comme Express.js).
Philosophie
- I/O Asynchrone (Non-bloquant) : C'est sa caractéristique principale. Node est conçu pour gérer des milliers de connexions simultanées (ex: chat, API) avec une faible empreinte mémoire.
- Single-Threaded (Mono-processus) : Node s'exécute sur un seul thread principal.
- Écosystème (NPM) : Le plus grand registre de paquets (librairies) au monde.
Node vs. PHP (FPM) vs. Python (Gunicorn)
La différence fondamentale est la gestion des requêtes.
| Critère | Node.js (Async I/O) | PHP-FPM / Python (Sync I/O) |
|---|---|---|
| Modèle | Asynchrone (Event Loop) | Synchrone (Multi-processus/threads) |
| Requête 1 (I/O Lente) | Démarre l'I/O (ex: BDD), libère le thread. | Démarre l'I/O, bloque le thread en attendant la BDD. |
| Requête 2 (Rapide) | Le même thread prend la Req 2 (pendant que Req 1 attend). | Doit attendre qu'un autre worker/thread soit libre. |
| Concurrence | Gère des milliers de connexions (I/O) sur 1 thread. | Gère N connexions (où N = nb de workers). |
| Usage | APIs, temps-réel (Sockets), microservices. | Applications métiers, CMS, CPU-bound tasks. |
L'Event Loop (Schéma simplifié)
Node est "mono-thread", mais il délègue les opérations I/O (lentes) au système (via libuv). L'Event Loop gère les retours (callbacks).
[Image d'un schéma Event Loop]
+----------------------+
| Call Stack | <-- (Code JS exécuté ici)
| (Ex: funcA()) |
+----------------------+
|
| (Opération I/O, ex: fs.readFile)
▼
+----------------------+
| API Node (libuv) | <-- (Géré par le Système/Threads C++)
| (Ex: "Lire fichier X") |
+----------------------+
|
| (Fichier lu, prêt)
▼
+----------------------+
| Callback Queue | <-- (Callback "fichierLu" attend)
| (FIFO) |
+----------------------+
|
| (Si Call Stack est vide...)
▼
+----------------------+
| EVENT LOOP | <-- (Prend le callback et l'envoie
| (Tourne en permanence) | au Call Stack)
+----------------------+
Code Bloquant (Synchrone) - MAUVAIS
readFileSync bloque le thread. Pendant 5 secondes, le serveur ne peut *rien* faire d'autre.
const fs = require('fs');
// 1. Le serveur bloque ici pendant 5s
const data = fs.readFileSync('/gros_fichier.txt');
console.log(data);
// 2. Cette ligne n'est atteinte qu'après 5s
console.log("Terminé");Code Non-Bloquant (Asynchrone) - BON
readFile délègue l'opération et passe un "callback". Le serveur continue de travailler.
const fs = require('fs');
// 1. Démarre la lecture (non-bloquant)
fs.readFile('/gros_fichier.txt', (err, data) => {
// 3. (Callback) Ceci s'exécute 5s plus tard
console.log(data);
});
// 2. Cette ligne s'affiche IMMÉDIATEMENT
console.log("Terminé");NVM (Node Version Manager)
La meilleure pratique. NVM permet d'installer et de basculer entre plusieurs versions de Node.js (ex: projet A en Node 18, projet B en Node 20).
Installation NVM (Linux/macOS)
# 1. Télécharger et exécuter le script curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 2. Recharger le shell source ~/.bashrc # (ou source ~/.zshrc) # 3. Vérifier nvm --version
Utilisation NVM
# Installer la dernière version LTS (Long Term Support) nvm install --lts # (ou Installer une version spécifique) nvm install 20 # Lister les versions installées nvm ls # Utiliser une version nvm use 20 # Définir la version par défaut nvm alias default 20
NVM for Windows
Un projet différent, mais avec le même objectif.
Allez sur : github.com/coreybutler/nvm-windows
- Téléchargez l'installeur (
nvm-setup.exe). - Exécutez-le.
Utilisation (Windows)
(Dans cmd ou PowerShell, en Admin) nvm list available nvm install 20.10.0 nvm use 20.10.0 nvm list
Alternative : Installeur Officiel
Sur nodejs.org. Installe Node globalement sur le système. Moins flexible que NVM.
Alternative : Docker
Idéal pour l'isolation et la production.
# Lancer un shell dans un conteneur Node docker run -it --rm -v $(pwd):/app -w /app node:20-alpine sh # (Dans le conteneur) node -v npm install
package.json
Le fichier manifeste de tout projet Node. Il décrit le projet, ses dépendances et ses scripts.
{
"name": "mon-projet-api",
"version": "1.0.0",
"description": "API pour IDEO-Lab",
"main": "app.js",
"type": "commonjs", // "commonjs" (require) ou "module" (import)
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.7.0"
}
}scripts: Raccourcis (npm run dev).dependencies: Requis pour la production (ex: Express).devDependencies: Requis pour le développement (ex: Nodemon).
Commandes npm
# 1. Initialiser un projet (crée package.json) npm init -y # 2. Installer une dépendance (Prod) # (Ajoute à 'dependencies' et au dossier 'node_modules') npm install express # (ou 'npm i express') # 3. Installer une dépendance (Dev) npm install --save-dev nodemon # (ou 'npm i -D nodemon') # 4. Installer toutes les dépendances (depuis package.json) npm install # 5. Lancer un script npm run dev npm start # (Raccourci pour 'npm run start') # 6. Lister les paquets npm list --depth=0 # 7. Désinstaller un paquet npm uninstall express # 8. 'npx' (Exécute un paquet sans l'installer) npx create-react-app mon-app
package-lock.json (Crucial)
Ce fichier est auto-généré. Ne *jamais* le modifier manuellement.
Rôle : Il "verrouille" (lock) les versions exactes de *toutes* les dépendances (et sous-dépendances) qui ont été installées.
Pourquoi ? package.json : "express": "^4.18.2" (^ = version 4.18.2 ou supérieure, mais < 5.0). package-lock.json : "express": "4.18.2" (la version exacte installée).
Quand vous lancez npm install, c'est le package-lock.json qui est utilisé pour garantir que tous les développeurs (et le serveur de prod) ont exactement le même node_modules. (Garantit la reproductibilité).
CommonJS (CJS) (Le "classique")
Le système de modules original de Node. Basé sur require() (synchrone).
math.js
function addition(a, b) {
return a + b;
}
const PI = 3.14;
// Exporter
module.exports = {
addition,
PI
};app.js
// Importer
const math = require('./math.js');
console.log(math.addition(10, 5)); // 15
console.log(math.PI); // 3.14Mode par défaut si "type": "commonjs" (ou absent) dans package.json.
ES Modules (ESM) (Le standard JavaScript)
Le système de modules standard de JavaScript (utilisé par les navigateurs et React). Basé sur import/export (asynchrone).
Activation : Ajoutez "type": "module" dans package.json.
math.mjs (ou .js)
export function addition(a, b) {
return a + b;
}
export const PI = 3.14;
// (Export par défaut)
// export default MonObjet;app.mjs (ou .js)
// Importer (destructuring)
import { addition, PI } from './math.mjs';
// (Import par défaut)
// import MonObjet from './math.mjs';
console.log(addition(10, 5)); // 15
console.log(PI); // 3.14C'est la norme moderne. Node.js supporte les deux, mais l'écosystème migre vers ESM.
fs (File System)
Le module fs fournit des méthodes asynchrones (non-bloquantes, avec callback) et synchrones (bloquantes, ...Sync).
Synchrone (Bloquant) - Pour les scripts
const fs = require('fs');
try {
const data = fs.readFileSync('config.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}Asynchrone (Callback) - Pour les serveurs
const fs = require('fs');
fs.readFile('config.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});fs/promises (La méthode moderne)
Depuis Node 14, fs/promises expose les méthodes fs en version Promise (utilisable avec async/await).
import { readFile, writeFile } from 'fs/promises';
async function main() {
try {
const data = await readFile('config.txt', 'utf8');
console.log(data);
await writeFile('output.txt', data.toUpperCase());
console.log('Fichier écrit !');
} catch (err) {
console.error(err);
}
}
main();path (Gestion des chemins)
Ne concaténez jamais les chemins (/ vs \). Utilisez path.
const path = require('path');
// 1. Joindre des chemins (multi-OS)
const configPath = path.join('/home', 'user', 'config.txt');
// '/home/user/config.txt'
// 2. Résoudre un chemin (depuis la racine)
const absPath = path.resolve('..', 'logs', 'app.log');
// '/home/ideo_user/projets/logs/app.log'
// 3. Obtenir le nom du dossier/fichier
path.dirname(configPath); // '/home/user'
path.basename(configPath); // 'config.txt'
path.extname(configPath); // '.txt'
// 4. __dirname (CJS)
// Le dossier du fichier actuel
const dbPath = path.join(__dirname, 'db.sqlite');
// (ESM - L'équivalent)
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);http (Serveur natif)
Le module http permet de créer un serveur web "bas niveau". (Express.js l'utilise en interne).
const http = require('http');
// 'req' (Request), 'res' (Response)
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Accueil\n');
} else if (req.url === '/json') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.writeHead(404);
res.end('Not Found\n');
}
});
server.listen(3000, '127.0.0.1', () => {
console.log('Serveur démarré sur http://127.0.0.1:3000');
});events (EventEmitter)
De nombreux objets Node (streams, http) héritent de EventEmitter. C'est le pattern "Observer" (Pub/Sub).
const EventEmitter = require('events');
class MonEmetteur extends EventEmitter {}
const emetteur = new MonEmetteur();
// 1. S'abonner à l'événement 'alerte'
emetteur.on('alerte', (data) => {
console.log('Alerte reçue !', data);
});
// (Une seule fois)
emetteur.once('connexion', () => {
console.log('Connecté !');
});
// 2. Émettre les événements
emetteur.emit('connexion');
emetteur.emit('alerte', { code: 'rouge' });
emetteur.emit('connexion'); // (Ne fait rien)Buffer (Données Binaires)
JavaScript n'était pas conçu pour les données binaires. Node a introduit Buffer pour gérer les octets (ex: lire une image, données TCP).
// Alloue un buffer de 10 octets (vide)
const buf1 = Buffer.alloc(10);
// Crée un buffer depuis un string
const buf2 = Buffer.from('Bonjour');
console.log(buf2);
// (Hex)
console.log(buf2.toString('utf8'));
// 'Bonjour' Stream (Flux de données)
Pour gérer des données volumineuses (ex: un fichier de 5Go), on ne peut pas tout charger en RAM (Buffer). On utilise un Stream (flux).
.pipe() est la méthode la plus efficace pour "tuyauter" un flux lisible (Readable) vers un flux écrivable (Writable).
const fs = require('fs');
const http = require('http');
// Cas 1: Mauvais (bloque 5Go de RAM)
http.createServer((req, res) => {
const data = fs.readFileSync('video.mp4'); // 5Go
res.end(data);
});
// Cas 2: Bon (Stream)
http.createServer((req, res) => {
// Crée un flux de lecture
const readStream = fs.createReadStream('video.mp4');
// "Pipe" le flux du fichier vers la réponse HTTP
// (La RAM reste faible)
readStream.pipe(res);
});Le "Callback Hell" (Pyramide)
L'ancienne méthode de gestion asynchrone consistait à passer des fonctions (callbacks) en dernier argument. Pour chaîner des opérations, on tombe dans le "Callback Hell".
Logique : 1. Lire config -> 2. Lire BDD -> 3. Écrire fichier
const fs = require('fs');
fs.readFile('config.json', 'utf8', (err1, configData) => {
if (err1) {
console.error("Erreur config", err1);
} else {
// (Supposez que 'db.query' est aussi async)
db.query(configData.sql, (err2, dbData) => {
if (err2) {
console.error("Erreur BDD", err2);
} else {
fs.writeFile('output.txt', dbData, (err3) => {
if (err3) {
console.error("Erreur écriture", err3);
} else {
console.log("Terminé !");
}
});
}
});
}
});Consommer des Promises (.then)
Une "Promise" est un objet qui représente une opération future. On "aplatit" le Callback Hell en chaînant les .then().
const { readFile, writeFile } = require('fs/promises');
readFile('config.json', 'utf8')
.then((configData) => {
// 1. Config lue
return db.query(configData.sql); // (Suppose que db.query retourne une Promise)
})
.then((dbData) => {
// 2. BDD lue
return writeFile('output.txt', dbData);
})
.then(() => {
// 3. Fichier écrit
console.log("Terminé !");
})
.catch((err) => {
// 4. Gère TOUTES les erreurs
console.error("Une erreur est survenue:", err);
})
.finally(() => {
// (Optionnel: s'exécute toujours)
db.close();
});Créer une Promise (Wrapper)
Pour "promisifier" une ancienne fonction à callback.
function oldCallbackFunc(callback) {
setTimeout(() => {
// callback(erreur, resultat)
callback(null, "OK");
}, 1000);
}
function promiseFunc() {
return new Promise((resolve, reject) => {
oldCallbackFunc((err, data) => {
if (err) {
reject(err); // En cas d'erreur
} else {
resolve(data); // En cas de succès
}
});
});
}Promise.all (Parallèle)
Exécute plusieurs promises en parallèle et attend qu'elles soient *toutes* terminées.
const p1 = fetch('https://api.example.com/users');
const p2 = fetch('https://api.example.com/products');
Promise.all([p1, p2])
.then(([usersResponse, productsResponse]) => {
// 'usersResponse' et 'productsResponse' sont prêts
})
.catch(err => {
// Échoue si UNE SEULE promise échoue
});Sucre syntaxique pour les Promises
async/await (ES2017) est une syntaxe qui permet d'écrire du code asynchrone (basé on Promises) comme s'il était synchrone.
async function: Déclare une fonction qui retourne implicitement une Promise.await: Met en "pause" l'exécution de la fonction (sans bloquer le thread) en attendant que la Promise soit résolue.try...catch: Gère les erreurs (les "rejects").
Exemple (remplace 3.2)
const { readFile, writeFile } = require('fs/promises');
// 1. 'async'
async function main() {
try {
// 2. 'await' (attend la résolution)
const configData = await readFile('config.json', 'utf8');
// 3. 'await' (attend la BDD)
const dbData = await db.query(configData.sql);
// 4. 'await' (attend l'écriture)
await writeFile('output.txt', dbData);
console.log("Terminé !");
} catch (err) {
// 5. 'catch' (gère les erreurs)
console.error("Une erreur est survenue:", err);
} finally {
db.close();
}
}
// Lancer la fonction
main();Le framework web de facto
Personne n'utilise le module http (cf 2.4) en production. Express.js est un micro-framework minimaliste qui simplifie le routage, la gestion des middlewares et les réponses.
Installation
npm install express
app.js (Hello World)
const express = require('express');
// 1. Créer l'application
const app = express();
const port = 3000;
// 2. Définir une route (le "Routing")
// (req = Request, res = Response)
app.get('/', (req, res) => {
// .send() est plus simple que .end()
res.send('Bonjour, IDEO-Lab !');
});
app.get('/json', (req, res) => {
// .json() gère le Content-Type auto
res.json({ status: 'ok' });
});
// 3. Démarrer le serveur
app.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}`);
});Routage (req.params, req.query, req.body)
const express = require('express');
const app = express();
// Middleware (requis pour parser le JSON des 'POST')
app.use(express.json());
// 1. GET (Query params: /search?q=test)
app.get('/search', (req, res) => {
const query = req.query.q; // "test"
res.json({ search: query });
});
// 2. GET (Path params: /users/123)
app.get('/users/:id', (req, res) => {
const userId = req.params.id; // "123"
res.json({ user_id: userId });
});
// 3. POST (Body: { "nom": "Alice" })
app.post('/users', (req, res) => {
const nom = req.body.nom; // "Alice"
// (Nécessite 'app.use(express.json())')
res.status(201).json({ status: 'créé', nom: nom });
});
// 4. PUT
app.put('/users/:id', (req, res) => {
// ...
});
// 5. DELETE
app.delete('/users/:id', (req, res) => {
// ...
});Middleware
Un "middleware" est une fonction qui s'exécute **entre** la requête et la route. ((req, res, next) => ...).
Middleware intégré (CORS)
# npm install cors
const cors = require('cors');
app.use(cors()); // Autorise toutes les originesMiddleware Custom (Logger)
// Ce middleware s'exécute sur TOUTES les requêtes
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
// 'next()' passe à la route suivante (ou au middleware suivant)
next();
});
// Servir des fichiers statiques
app.use(express.static('public'));
// (http://localhost:3000/images/logo.png -> /public/images/logo.png)npm install pg
node-postgres (pg) est le driver principal pour PostgreSQL. Il est entièrement asynchrone (basé sur Promises).
Configuration (Pool de connexions)
Ne créez pas un client par requête. Créez un **Pool** (singleton) et réutilisez-le.
// db.js
const { Pool } = require('pg');
const pool = new Pool({
user: 'db_user',
host: 'localhost',
database: 'ideo_lab_db',
password: 'db_password',
port: 5432,
});
module.exports = {
query: (text, params) => pool.query(text, params),
};Utilisation (Async/Await)
// app.js
const express = require('express');
const db = require('./db');
const app = express();
app.get('/users', async (req, res) => {
try {
const { rows } = await db.query('SELECT * FROM users');
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/users/:id', async (req, res) => {
try {
const { id } = req.params;
// Protection contre l'injection SQL
const { rows } = await db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
res.json(rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});Axios
Pour appeler d'autres APIs (microservices, APIs externes) depuis Node, axios est la librairie la plus populaire (basée sur Promises).
npm install axios
GET
const axios = require('axios');
async function getUsers() {
try {
const response = await axios.get('https://api.example.com/users');
console.log(response.data); // Données JSON
} catch (error) {
console.error(error.response.data); // Erreur de l'API
}
}POST
async function createUser(nom) {
try {
const response = await axios.post('https://api.example.com/users', {
nom: nom,
status: 'actif'
});
console.log(response.data);
} catch (error) {
console.error(error);
}
}
// (axios.put, axios.delete...)Le problème de node app.js
Si vous lancez node app.js en production :
- Si l'app crashe (erreur), le processus s'arrête. Le site est mort.
- Si le serveur reboote, le site ne redémarre pas.
- Il ne tourne que sur 1 cœur de CPU.
Solution : PM2 (Process Manager)
PM2 est un gestionnaire de processus pour Node.js. Il gère :
- Clustering : Lance 1 processus par cœur de CPU (Mode Cluster) et répartit la charge (load balancing).
- Monitoring : Surveille l'état (RAM, CPU) des processus.
- Auto-Restart : Redémarre l'app si elle crashe.
- Startup Script : Redémarre l'app au boot du serveur.
- Logs : Centralise les logs (
stdout/stderr).
Commandes PM2 (Production)
# 1. Installer PM2 (Globalement) sudo npm install -g pm2 # 2. Démarrer l'application (Mode Cluster) # (-i max = 1 worker par coeur CPU) pm2 start app.js -i max --name "api-ideo-lab" # 3. Lister les processus pm2 list # (ou 'pm2 ls') # 4. Voir les logs (en direct) pm2 logs api-ideo-lab # (ou 'pm2 logs') # 5. Redémarrer (0-downtime reload) pm2 reload api-ideo-lab # 6. Arrêter pm2 stop api-ideo-lab # 7. Supprimer pm2 delete api-ideo-lab # 8. (Crucial) Générer le script de démarrage pm2 startup # (Copiez/Collez la commande générée) pm2 save
Architecture de Production
PM2 (Node.js) écoute sur un port local (ex: 3000). Nginx écoute sur le port public (80/443) et agit en "reverse proxy".
[Internet] (Port 80/443)
|
▼
[Nginx (Reverse Proxy)]
(TLS, Cache, Compression)
|
| (proxy_pass)
▼
[PM2 / Node.js] (Port 3000)
(Clustered)
Configuration Nginx
# /etc/nginx/sites-available/node-app
upstream nodejs_app {
# PM2 écoute ici (localhost:3000)
server 127.0.0.1:3000;
}
server {
listen 80;
server_name node-app.ideolab.com;
location / {
proxy_pass http://nodejs_app;
# Headers essentiels
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Pour les WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}NVM (Gestionnaire de versions)
nvm install --lts nvm install 20 nvm use 20 nvm ls nvm alias default 20
NPM (Gestionnaire de paquets)
npm init -y npm install express npm i -D nodemon npm install # (ou 'npm i') npm run dev npm run start npm list --depth=0 npm uninstall express
Modules (Core)
// CJS (Défaut)
const fs = require('fs');
module.exports = { ... };
// ESM ("type": "module")
import fs from 'fs';
export { ... };
export default ...;
// FS (Async/Promise)
import { readFile, writeFile } from 'fs/promises';
const data = await readFile('file.txt', 'utf8');
// Path
const path = require('path');
path.join(__dirname, 'views');
path.resolve('..', 'file.txt');