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

  1. process.nextTick() — avant chaque phase
  2. Promise.then() — après nextTick, avant la phase
  3. setTimeout(fn, 0) — phase timers
  4. setImmediate() — 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é

Featurenpmpnpmyarn
Disk spaceCopieHard linksCopie
Install speedLentTrès rapideRapide
StrictnessFlexibleStrict (isolated)Flexible
MonorepoWorkspacesWorkspacesWorkspaces
Plug'n'PlayNonNonOui
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èreExpressFastify
Performance~25k req/s~80k req/s
JSON serializationJSON.stringifyfast-json-stringify (schema-based)
Schema validationNon (Joi/zod)JSON Schema natif
Plugin systemNonEncapsulé (graphique)
TypeScriptMoyenExcellent
OpenAPIswagger-jsdoc@fastify/swagger (auto)
MiddlewareCallbackSchema-based + callback
LoggingMorganPino (intégré)
Bundle size154 kB118 kB

Fastify : points clés

  1. JSON Schema serialization : sérialise 2-3x plus vite
  2. Plugin encapsulation : contextes isolés (héritage parent)
  3. Hooks : onRequest, preValidation, preHandler, onSend, onResponse
  4. Validation : JSON Schema pour input/output
  5. 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

  1. Chain of Responsibility : chaque middleware décide de passer ou non
  2. Decorator : enrichit request/response
  3. Error boundary : capture les erreurs (Express 4+)

6. Streams & Buffer

Stream types (Node.js)

TypeDescriptionExemple
ReadableSource de donnéesfs.createReadStream
WritableDestinationfs.createWriteStream
DuplexReadable + Writablenet.Socket
TransformTransforme les donnéeszlib.createGzip

Stream modes

  • Flowing : data events automatically (pipe() ou resume())
  • 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)