Modern Backend Engineering
Chapitre 2
Chapitre 02 — Node.js
Chapitre 02 — Node.js
Cours complet — Node.js
1. Event Loop (libuv)
Phases de l'event loop
┌───────────────────────────┐
┌─►│ timers │ ← setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← I/O callbacks différés
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← usage interne libuv
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← attente I/O (epoll/kqueue)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │ ← socket.on('close', ...)
│ └───────────────────────────┘
└─────────────────────────────── loop
Ordre d'exécution :
process.nextTick()— avant chaque phasePromise.then()— après nextTick, avant la phasesetTimeout(fn, 0)— phase timerssetImmediate()— phase check
nextTick queue : prioritaire sur tout. Peut causer I/O starvation.
libuv internals
- Thread pool : 4 threads par défaut (UV_THREADPOOL_SIZE)
- Utilisé pour : fs, DNS, crypto, zlib
- epoll (Linux), kqueue (macOS), IOCP (Windows)
- Async handles : signal, timer, idle, prepare, check, poll
// Démontrer l'ordre
setTimeout(() => console.log('1: timer'), 0)
setImmediate(() => console.log('2: immediate'))
process.nextTick(() => console.log('3: nextTick'))
Promise.resolve().then(() => console.log('4: promise'))
// Output: 3 → 4 → 1 → 2 (ou 2 → 1 selon la phase)
2. V8 Engine
Architecture
JS Source → Parser → AST → Ignition (Interpreter) → Bytecode
↓ (hot code)
TurboFan (JIT Compiler) → Machine Code
- Ignition : interpréteur bytecode (rapide à démarrer)
- TurboFan : compilateur JIT (optimise le hot path)
- Orinoco : garbage collector (generational, concurrent)
- Pointer compression : réduit mémoire heap de 40% (Node 14+)
V8 optimizations
- Hidden classes : cartes de shapes d'objets
- Inline caching : caches de type pour appels de méthode
- Elements kinds : types de tableaux (PACKED_SMI, PACKED_DOUBLE, etc.)
- Deoptimization : retour à bytecode si les hypothèses changent
// Monomorphic vs Polymorphic
function add(a, b) { return a + b }
add(1, 2) // monomorphic
add(1, 2) // same types → optimized
add('a', 'b') // polymorphic → déoptimization
add([1], [2]) // megamorphic → pas d'optimisation
Conseils perf V8 :
- Types stables pour les objets
- Éviter les delete sur des objets chauds
- Préférer les tableaux homogènes
- Éviter les try/catch dans les hot paths
- Utiliser les classes (ES6) plutôt que prototypes manuels
3. npm / pnpm / yarn
Package Managers comparé
| Feature | npm | pnpm | yarn |
|---|---|---|---|
| Disk space | Copie | Hard links | Copie |
| Install speed | Lent | Très rapide | Rapide |
| Strictness | Flexible | Strict (isolated) | Flexible |
| Monorepo | Workspaces | Workspaces | Workspaces |
| Plug'n'Play | Non | Non | Oui |
| Popularité | 100% | 20% | 15% |
pnpm avantages
- Node_modules en dur : espace disque économisé
- Isolation stricte : ne peut accéder qu'aux dépendances déclarées
- Rapide : parallélisation, caching
- Monorepo natif : meilleur que npm workspaces
# Installation
npm install -g pnpm
# Commandes (similaries)
pnpm add express
pnpm add -D typescript
pnpm run build
4. Express vs Fastify
Comparaison
| Critère | Express | Fastify |
|---|---|---|
| Performance | ~25k req/s | ~80k req/s |
| JSON serialization | JSON.stringify | fast-json-stringify (schema-based) |
| Schema validation | Non (Joi/zod) | JSON Schema natif |
| Plugin system | Non | Encapsulé (graphique) |
| TypeScript | Moyen | Excellent |
| OpenAPI | swagger-jsdoc | @fastify/swagger (auto) |
| Middleware | Callback | Schema-based + callback |
| Logging | Morgan | Pino (intégré) |
| Bundle size | 154 kB | 118 kB |
Fastify : points clés
- JSON Schema serialization : sérialise 2-3x plus vite
- Plugin encapsulation : contextes isolés (héritage parent)
- Hooks : onRequest, preValidation, preHandler, onSend, onResponse
- Validation : JSON Schema pour input/output
- Logger : Pino intégré (le plus rapide des logger Node.js)
import Fastify from 'fastify'
const app = Fastify({ logger: true })
// Schema de validation
const userSchema = {
params: { type: 'object', properties: { id: { type: 'integer' } } },
response: {
200: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
}
}
}
}
app.get('/user/:id', { schema: userSchema }, async (req) => {
return { id: req.params.id, name: 'Alice' }
})
5. Middleware Pattern
Express-style
// Chaine de middleware
app.use(cors())
app.use(express.json())
app.use(authMiddleware)
app.use('/api', apiRouter)
app.use(errorHandler)
// Middleware = (req, res, next)
function authMiddleware(req, res, next) {
const token = req.headers.authorization
if (!token) return res.status(401).json({ error: 'Unauthorized' })
req.user = verifyToken(token)
next() // Passe au suivant
}
Fastify-style (hooks)
app.addHook('onRequest', async (req) => {
// Avant validation
})
app.addHook('preValidation', async (req) => {
// Avant validation schéma
})
app.addHook('preHandler', async (req) => {
// Après validation
})
app.addHook('onSend', async (req, reply, payload) => {
// Avant envoi réponse
return payload // Modifier payload
})
app.addHook('onResponse', async (req, reply) => {
// Après envoi
})
Middleware design patterns
- Chain of Responsibility : chaque middleware décide de passer ou non
- Decorator : enrichit request/response
- Error boundary : capture les erreurs (Express 4+)
6. Streams & Buffer
Stream types (Node.js)
| Type | Description | Exemple |
|---|---|---|
| Readable | Source de données | fs.createReadStream |
| Writable | Destination | fs.createWriteStream |
| Duplex | Readable + Writable | net.Socket |
| Transform | Transforme les données | zlib.createGzip |
Stream modes
- Flowing : data events automatically (
pipe()ouresume()) - Paused :
read()manuel - Object mode : streams d'objets (pas seulement buffers)
// Pipeline (recommandé vs pipe())
import { pipeline } from 'stream/promises'
import { createReadStream, createWriteStream } from 'fs'
import { createGzip } from 'zlib'
await pipeline(
createReadStream('source.txt'),
createGzip(),
createWriteStream('source.txt.gz')
)
console.log('Compression terminée')
// Avec gestion d'erreur automatique
Buffer
// Allouer
const buf = Buffer.alloc(1024) // Zero-filled
const buf2 = Buffer.from('hello') // From string
const buf3 = Buffer.from([0x48, 0x65]) // From array
// Lire/écrire
buf.write('hello', 0, 'utf8')
buf.readUInt32BE(0)
buf.toString('utf8', 0, 5)
// Buffer pooling (Smalloc)
// Les buffers < 8KB sont poolés (partagent un ArrayBuffer)
7. Cluster Mode
Architecture
import cluster from 'cluster'
import { cpus } from 'os'
import http from 'http'
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`)
// Fork workers
const numCPUs = cpus().length
for (let i = 0; i < numCPUs; i++) {
cluster.fork()
}
// Restart on crash
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`)
cluster.fork()
})
// Graceful shutdown
process.on('SIGTERM', () => {
for (const id in cluster.workers) {
cluster.workers[id].kill('SIGTERM')
}
process.exit(0)
})
} else {
// Worker processes
const server = http.createServer((req, res) => {
res.end(`Handled by ${process.pid}`)
})
server.listen(3000, () => {
console.log(`Worker ${process.pid} started`)
})
process.on('SIGTERM', () => {
server.close(() => process.exit(0))
})
}
Round-robin (Windows, ou par défaut) : le primary distribue les connexions. Shared socket (Linux) : les workers partagent le handle via SO_REUSEPORT.
PM2 alternative
pm2 start app.js -i max
pm2 list
pm2 monit
pm2 reload app.js
8. Error Handling
Patterns
// 1. Async handler wrapper (Express)
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
// 2. Error class custom
class AppError extends Error {
constructor(message, statusCode, code) {
super(message)
this.statusCode = statusCode
this.code = code
this.isOperational = true
}
}
// 3. Global error handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500
const body = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.isOperational ? err.message : 'Internal Server Error',
}
}
if (!err.isOperational) {
console.error('Unexpected error:', err)
}
res.status(statusCode).send(body)
})
// 4. Unhandled rejections
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason)
// Process.exit(1) en production
})
// 5. Uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err)
process.exit(1)
})
9. Node 22+ Features
V8 12.4+ (Node 22)
- Array.fromAsync : créer un array depuis un async iterable
- Set methods : union, intersection, difference, symmetricDifference
- Promise.withResolvers : créer Promise sans executer
- Error.cause : chaînage d'erreurs
// Promise.withResolvers
const { promise, resolve, reject } = Promise.withResolvers()
setTimeout(() => resolve('done'), 1000)
// Error cause
try {
await riskyOperation()
} catch (err) {
throw new Error('Operation failed', { cause: err })
}
// Set operations
const a = new Set([1, 2, 3])
const b = new Set([3, 4, 5])
a.intersection(b) // Set { 3 }
a.union(b) // Set { 1, 2, 3, 4, 5 }
a.difference(b) // Set { 1, 2 }
Node 22 runtime features
- WebSocket client natif (pas de npm ws)
- --experimental-require-module : require ESM
- SQLite intégré (--experimental-sqlite)
- node:test runner stable
// node:test (built-in)
import { test, describe, it, mock } from 'node:test'
import assert from 'node:assert'
test('async test', async () => {
const result = await fetch('http://localhost:3000/api')
assert.strictEqual(result.status, 200)
})
// WebSocket client natif
const ws = new WebSocket('ws://localhost:3000')
ws.onmessage = (event) => console.log(event.data)
// SQLite natif (experimental)
import Database from 'node:sqlite'
const db = new Database('test.db')
db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
Module system
- ESM par défaut (package.json "type": "module")
- require() pour CJS
- import.meta.resolve : résolution de modules
- import.meta.dirname : équivalent __dirname en ESM
Références
- Node.js official docs (nodejs.org)
- libuv documentation (docs.libuv.org)
- V8 developer blog (v8.dev)
- Fastify documentation (fastify.dev)