Questions d'entretien Node.js backend : Guide complet 2026

Les 25 questions d'entretien Node.js backend les plus fréquentes. Event loop, async/await, streams, clustering et performance expliqués avec des réponses détaillées.

Questions d'entretien Node.js backend - Guide complet

Les entretiens Node.js backend évaluent la compréhension des mécanismes internes du runtime, la maîtrise de l'asynchrone et la capacité à concevoir des applications performantes. Ce guide couvre les questions les plus fréquentes, des fondamentaux jusqu'aux concepts avancés de production.

Conseil pour l'entretien

Les recruteurs apprécient les réponses qui combinent théorie et exemples pratiques. Pour chaque question, illustrer avec du code ou un cas d'usage concret démontre une expérience réelle.

Fondamentaux de Node.js

Question 1 : Qu'est-ce que l'Event Loop et comment fonctionne-t-elle ?

L'Event Loop est le mécanisme central qui permet à Node.js de gérer des opérations asynchrones de manière non-bloquante malgré son exécution single-threaded. Elle orchestre l'exécution du code JavaScript, des callbacks et des événements.

event-loop-demo.jsjavascript
// Démonstration de l'ordre d'exécution de l'Event Loop

console.log('1. Script start (synchrone)');

// setTimeout va dans la Timer Queue
setTimeout(() => {
  console.log('5. setTimeout callback (Timer Queue)');
}, 0);

// setImmediate va dans la Check Queue
setImmediate(() => {
  console.log('6. setImmediate callback (Check Queue)');
});

// Promise va dans la Microtask Queue (prioritaire)
Promise.resolve().then(() => {
  console.log('3. Promise.then (Microtask Queue)');
});

// process.nextTick a la plus haute priorité
process.nextTick(() => {
  console.log('2. process.nextTick (nextTick Queue)');
});

console.log('4. Script end (synchrone)');

// Sortie dans l'ordre : 1, 4, 2, 3, 5, 6

L'Event Loop suit un ordre précis de traitement des queues : d'abord le code synchrone, puis nextTick, les microtasks (Promises), les timers, les I/O callbacks, setImmediate, et enfin les close callbacks.

Question 2 : Quelle est la différence entre process.nextTick() et setImmediate() ?

Cette question évalue la compréhension fine des priorités d'exécution dans l'Event Loop.

nextTick-vs-immediate.jsjavascript
// Comparaison des comportements

// process.nextTick s'exécute AVANT la prochaine phase de l'Event Loop
process.nextTick(() => {
  console.log('nextTick 1');
  process.nextTick(() => {
    console.log('nextTick 2 (imbriqué)');
  });
});

// setImmediate s'exécute dans la phase Check de l'Event Loop
setImmediate(() => {
  console.log('setImmediate 1');
  setImmediate(() => {
    console.log('setImmediate 2 (imbriqué)');
  });
});

// Sortie : nextTick 1, nextTick 2, setImmediate 1, setImmediate 2

process.nextTick() est traité immédiatement après l'opération courante, avant que l'Event Loop ne continue. Un usage excessif peut bloquer l'Event Loop. setImmediate() est plus prévisible et recommandé pour différer l'exécution.

Attention au starvation

Un appel récursif à process.nextTick() peut affamer l'Event Loop et empêcher le traitement des I/O. Préférer setImmediate() pour les opérations non critiques.

Question 3 : Comment Node.js gère-t-il les erreurs dans le code asynchrone ?

La gestion des erreurs asynchrones diffère fondamentalement du code synchrone. Sans gestion appropriée, une erreur peut crasher l'application.

error-handling.jsjavascript
// Patterns de gestion d'erreurs asynchrones

// Pattern 1 : Callbacks avec error-first
function readFileCallback(path, callback) {
  const fs = require('fs');
  fs.readFile(path, 'utf8', (err, data) => {
    if (err) {
      // L'erreur est TOUJOURS le premier argument
      return callback(err, null);
    }
    callback(null, data);
  });
}

// Pattern 2 : Promises avec catch
async function readFilePromise(path) {
  const fs = require('fs').promises;
  try {
    const data = await fs.readFile(path, 'utf8');
    return data;
  } catch (err) {
    // Gestion centralisée des erreurs
    console.error(`Erreur lecture fichier: ${err.message}`);
    throw err; // Re-throw pour propagation
  }
}

// Pattern 3 : Gestion globale des rejections non gérées
process.on('unhandledRejection', (reason, promise) => {
  console.error('Rejection non gérée:', reason);
  // En production : logger et graceful shutdown
});

// Pattern 4 : Gestion des exceptions non catchées
process.on('uncaughtException', (err) => {
  console.error('Exception non catchée:', err);
  // CRITIQUE : toujours terminer le process après
  process.exit(1);
});

En production, chaque Promise doit avoir un .catch() ou être dans un bloc try/catch. Les gestionnaires globaux servent de filet de sécurité, pas de solution principale.

Asynchrone et Concurrence

Question 4 : Expliquez la différence entre parallélisme et concurrence en Node.js

Node.js est concurrent mais pas parallèle par défaut. Cette distinction est fondamentale pour comprendre les performances.

concurrency-vs-parallelism.jsjavascript
// CONCURRENCE : plusieurs tâches progressent en alternance (single-thread)
async function concurrentTasks() {
  console.time('concurrent');

  // Ces appels sont concurrents, pas parallèles
  const results = await Promise.all([
    fetch('https://api.example.com/users'),    // I/O non-bloquant
    fetch('https://api.example.com/products'), // I/O non-bloquant
    fetch('https://api.example.com/orders'),   // I/O non-bloquant
  ]);

  console.timeEnd('concurrent'); // ~temps de la requête la plus longue
  return results;
}

// PARALLÉLISME : avec Worker Threads pour le CPU-bound
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  // Le thread principal délègue le travail CPU-intensive
  async function parallelComputation() {
    console.time('parallel');

    const workers = [
      createWorker({ start: 0, end: 1000000 }),
      createWorker({ start: 1000000, end: 2000000 }),
      createWorker({ start: 2000000, end: 3000000 }),
    ];

    const results = await Promise.all(workers);
    console.timeEnd('parallel');
    return results.reduce((a, b) => a + b, 0);
  }

  function createWorker(data) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData: data });
      worker.on('message', resolve);
      worker.on('error', reject);
    });
  }
} else {
  // Code exécuté dans le Worker Thread
  const { workerData } = require('worker_threads');
  let sum = 0;
  for (let i = workerData.start; i < workerData.end; i++) {
    sum += Math.sqrt(i); // Calcul CPU-intensive
  }
  parentPort.postMessage(sum);
}

Pour les opérations I/O-bound (réseau, fichiers), la concurrence native suffit. Pour le CPU-bound (calculs lourds, crypto), Worker Threads permettent le vrai parallélisme.

Question 5 : Comment fonctionne le module Cluster ?

Le module Cluster permet de créer plusieurs processus Node.js partageant le même port, exploitant ainsi tous les cœurs CPU disponibles.

cluster-example.jsjavascript
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  console.log(`Forking ${numCPUs} workers...`);

  // Fork un worker par cœur CPU
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // Gestion des workers qui crashent
  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code})`);
    console.log('Starting a new worker...');
    cluster.fork(); // Redémarrage automatique
  });

  // Communication inter-processus
  cluster.on('message', (worker, message) => {
    console.log(`Message from worker ${worker.id}:`, message);
  });

} else {
  // Les workers partagent le port TCP
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);

    // Envoyer des stats au primary
    process.send({ type: 'request', pid: process.pid });
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

Le load balancing est géré automatiquement par le système d'exploitation (round-robin sur Linux/macOS). En production, PM2 simplifie cette gestion avec son mode cluster intégré.

Prêt à réussir tes entretiens Node.js / NestJS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Streams et Buffers

Question 6 : Quand utiliser les Streams plutôt que les méthodes classiques ?

Les Streams permettent de traiter des données par morceaux (chunks) au lieu de charger l'intégralité en mémoire. Essentiels pour les fichiers volumineux et le streaming.

streams-comparison.jsjavascript
const fs = require('fs');

// ❌ MAUVAIS : charge tout le fichier en mémoire
async function readEntireFile(path) {
  const data = await fs.promises.readFile(path); // Bloque si fichier > RAM
  return processData(data);
}

// ✅ BON : traitement par chunks avec Stream
function readWithStream(path) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    const readStream = fs.createReadStream(path, {
      highWaterMark: 64 * 1024, // 64KB par chunk
    });

    readStream.on('data', (chunk) => {
      // Traitement progressif, mémoire constante
      chunks.push(processChunk(chunk));
    });

    readStream.on('end', () => resolve(chunks));
    readStream.on('error', reject);
  });
}

// ✅ MEILLEUR : pipeline pour chaîner les transformations
const { pipeline } = require('stream/promises');
const zlib = require('zlib');

async function compressFile(input, output) {
  await pipeline(
    fs.createReadStream(input),   // Source
    zlib.createGzip(),            // Transform
    fs.createWriteStream(output)  // Destination
  );
  // Gestion automatique des erreurs et du backpressure
}

Utiliser les Streams dès que la taille des données peut dépasser quelques MB, ou pour le traitement temps réel (uploads, logs, données réseau).

Question 7 : Expliquez le concept de backpressure

Le backpressure survient quand le producteur de données est plus rapide que le consommateur. Sans gestion, la mémoire explose.

backpressure-demo.jsjavascript
const fs = require('fs');

// ❌ Problème : pas de gestion du backpressure
function badCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  readable.on('data', (chunk) => {
    // Si write() retourne false, le buffer interne est plein
    // Mais ici on continue à lire quand même → memory leak
    writable.write(chunk);
  });
}

// ✅ Solution : respecter le signal du writable
function goodCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  readable.on('data', (chunk) => {
    const canContinue = writable.write(chunk);

    if (!canContinue) {
      // Pause la lecture jusqu'à ce que le buffer se vide
      readable.pause();
    }
  });

  writable.on('drain', () => {
    // Buffer vidé, reprendre la lecture
    readable.resume();
  });

  readable.on('end', () => writable.end());
}

// ✅ MEILLEUR : pipe() gère tout automatiquement
function bestCopy(src, dest) {
  const readable = fs.createReadStream(src);
  const writable = fs.createWriteStream(dest);

  // pipe() gère le backpressure nativement
  readable.pipe(writable);
}

La méthode pipe() ou pipeline() gère automatiquement le backpressure. Pour les cas complexes, implémenter la logique pause/resume manuellement.

Performance et Optimisation

Question 8 : Comment identifier et résoudre les fuites mémoire ?

Les fuites mémoire sont fréquentes en Node.js. Savoir les détecter et les corriger est essentiel en production.

memory-leak-patterns.jsjavascript
// ❌ Fuite 1 : closures qui retiennent des références
function createLeakyHandler() {
  const hugeData = Buffer.alloc(100 * 1024 * 1024); // 100MB

  return function handler(req, res) {
    // hugeData reste en mémoire tant que handler existe
    res.end('Hello');
  };
}

// ✅ Correction : limiter la portée
function createSafeHandler() {
  return function handler(req, res) {
    // Données créées et libérées à chaque requête
    const data = fetchData();
    res.end(data);
  };
}

// ❌ Fuite 2 : event listeners non nettoyés
class LeakyClass {
  constructor() {
    // Ajout à chaque instanciation, jamais retiré
    process.on('message', this.handleMessage);
  }
  handleMessage(msg) { /* ... */ }
}

// ✅ Correction : cleanup explicite
class SafeClass {
  constructor() {
    this.boundHandler = this.handleMessage.bind(this);
    process.on('message', this.boundHandler);
  }
  handleMessage(msg) { /* ... */ }

  destroy() {
    // Nettoyage obligatoire
    process.removeListener('message', this.boundHandler);
  }
}

// Diagnostic avec les outils natifs
function diagnoseMemory() {
  const used = process.memoryUsage();
  console.log({
    heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
    external: `${Math.round(used.external / 1024 / 1024)}MB`,
    rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
  });
}

// Activer le garbage collector manuel pour les tests
// node --expose-gc app.js
if (global.gc) {
  global.gc();
  diagnoseMemory();
}

En production, utiliser des outils comme clinic.js, les heap snapshots de Chrome DevTools, ou des APM (Application Performance Monitoring) comme DataDog ou New Relic.

Question 9 : Comment optimiser les performances d'une API Node.js ?

Cette question évalue la connaissance des techniques d'optimisation à plusieurs niveaux.

performance-optimization.jsjavascript
// 1. CACHING : réduire les appels coûteux
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // TTL 5 minutes

async function getCachedUser(id) {
  const cacheKey = `user:${id}`;
  let user = cache.get(cacheKey);

  if (!user) {
    user = await db.users.findById(id);
    cache.set(cacheKey, user);
  }

  return user;
}

// 2. CONNECTION POOLING : réutiliser les connexions DB
const { Pool } = require('pg');
const pool = new Pool({
  max: 20,                // Connexions simultanées max
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// 3. COMPRESSION : réduire la taille des réponses
const compression = require('compression');
app.use(compression({
  filter: (req, res) => {
    // Compresser seulement si > 1KB
    return compression.filter(req, res);
  },
  threshold: 1024,
}));

// 4. BATCHING : grouper les opérations
async function batchInsert(items) {
  const BATCH_SIZE = 1000;

  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    await db.items.insertMany(batch);
  }
}

// 5. LAZY LOADING : charger à la demande
async function getUserWithPosts(userId, includePosts = false) {
  const user = await db.users.findById(userId);

  if (includePosts) {
    user.posts = await db.posts.findByUserId(userId);
  }

  return user;
}

Les optimisations doivent être guidées par le profiling. Mesurer avant d'optimiser pour identifier les vrais goulots d'étranglement.

Règle des 80/20

80% des problèmes de performance viennent de 20% du code. Utiliser le profiling pour identifier ces zones critiques avant d'optimiser à l'aveugle.

Sécurité

Question 10 : Comment protéger une API Node.js contre les attaques courantes ?

La sécurité est un sujet récurrent en entretien. Démontrer une connaissance des vulnérabilités OWASP est attendu.

security-best-practices.jsjavascript
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

const app = express();

// 1. HEADERS DE SÉCURITÉ avec Helmet
app.use(helmet());

// 2. RATE LIMITING contre les attaques brute-force
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requêtes par IP
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});
app.use('/api/', limiter);

// 3. SANITIZATION contre les injections NoSQL
app.use(mongoSanitize());

// 4. PROTECTION XSS
app.use(xss());

// 5. VALIDATION stricte des entrées
const { body, validationResult } = require('express-validator');

app.post('/api/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).escape(),
    body('name').trim().escape(),
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Continuer le traitement
  }
);

// 6. PROTECTION contre les injections SQL (avec paramètres)
async function safeQuery(userId) {
  // ✅ Requête paramétrée
  const result = await pool.query(
    'SELECT * FROM users WHERE id = $1',
    [userId]
  );
  return result.rows;
}

// ❌ JAMAIS de concaténation de chaînes
async function unsafeQuery(userId) {
  // Vulnérable à l'injection SQL
  const result = await pool.query(
    `SELECT * FROM users WHERE id = ${userId}`
  );
}

En production, ajouter également : CORS restrictif, HTTPS obligatoire, logs de sécurité, rotation des secrets, et audits réguliers des dépendances (npm audit).

Architecture et Design Patterns

Question 11 : Expliquez le pattern Repository en Node.js

Le pattern Repository abstrait l'accès aux données et facilite les tests et la maintenabilité.

repository-pattern.jsjavascript
// Interface abstraite (pour TypeScript, ou documentation)
class UserRepository {
  async findById(id) { throw new Error('Not implemented'); }
  async findByEmail(email) { throw new Error('Not implemented'); }
  async create(userData) { throw new Error('Not implemented'); }
  async update(id, userData) { throw new Error('Not implemented'); }
  async delete(id) { throw new Error('Not implemented'); }
}

// Implémentation concrète avec Prisma
class PrismaUserRepository extends UserRepository {
  constructor(prisma) {
    super();
    this.prisma = prisma;
  }

  async findById(id) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async findByEmail(email) {
    return this.prisma.user.findUnique({ where: { email } });
  }

  async create(userData) {
    return this.prisma.user.create({ data: userData });
  }

  async update(id, userData) {
    return this.prisma.user.update({
      where: { id },
      data: userData,
    });
  }

  async delete(id) {
    return this.prisma.user.delete({ where: { id } });
  }
}

// Implémentation pour les tests
class InMemoryUserRepository extends UserRepository {
  constructor() {
    super();
    this.users = new Map();
    this.idCounter = 1;
  }

  async findById(id) {
    return this.users.get(id) || null;
  }

  async create(userData) {
    const user = { id: this.idCounter++, ...userData };
    this.users.set(user.id, user);
    return user;
  }
  // ... autres méthodes
}

// Service utilisant le repository (injection de dépendances)
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async getUser(id) {
    const user = await this.userRepository.findById(id);
    if (!user) throw new Error('User not found');
    return user;
  }
}

Ce pattern permet de changer l'implémentation de la persistance sans modifier la logique métier.

Question 12 : Comment implémenter un système de queue de tâches ?

Les queues permettent de différer les tâches lourdes et d'assurer leur exécution fiable.

job-queue.jsjavascript
const Queue = require('bull');

// Création de la queue avec Redis comme backend
const emailQueue = new Queue('email', {
  redis: {
    host: 'localhost',
    port: 6379,
  },
  defaultJobOptions: {
    attempts: 3,          // Nombre de tentatives
    backoff: {
      type: 'exponential',
      delay: 2000,        // Délai initial entre les tentatives
    },
    removeOnComplete: 100, // Garder les 100 derniers jobs complétés
  },
});

// Producteur : ajouter des jobs à la queue
async function sendWelcomeEmail(userId, email) {
  await emailQueue.add('welcome', {
    userId,
    email,
    template: 'welcome',
  }, {
    priority: 1,          // Priorité haute
    delay: 5000,          // Délai de 5 secondes
  });
}

// Consommateur : traiter les jobs
emailQueue.process('welcome', async (job) => {
  const { userId, email, template } = job.data;

  // Mise à jour de la progression
  job.progress(10);

  const html = await renderTemplate(template, { userId });
  job.progress(50);

  await sendEmail(email, 'Bienvenue!', html);
  job.progress(100);

  return { sent: true, email };
});

// Gestion des événements
emailQueue.on('completed', (job, result) => {
  console.log(`Job ${job.id} completed:`, result);
});

emailQueue.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err.message);
});

// Jobs récurrents (cron)
emailQueue.add('newsletter', { type: 'weekly' }, {
  repeat: {
    cron: '0 9 * * MON', // Tous les lundis à 9h
  },
});

Bull avec Redis est la solution la plus populaire. Pour des besoins plus simples, agenda ou bee-queue sont des alternatives légères.

Questions avancées

Question 13 : Comment fonctionne le module natif N-API ?

N-API permet de créer des modules natifs en C/C++ avec une API stable entre les versions de Node.js.

native-module.cppcpp
// Module natif pour calculs CPU-intensifs

#include <napi.h>

// Fonction synchrone exposée à JavaScript
Napi::Number Fibonacci(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // Validation des arguments
  if (info.Length() < 1 || !info[0].IsNumber()) {
    Napi::TypeError::New(env, "Number expected")
      .ThrowAsJavaScriptException();
    return Napi::Number::New(env, 0);
  }

  int n = info[0].As<Napi::Number>().Int32Value();

  // Calcul itératif de Fibonacci
  long long a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    long long temp = a + b;
    a = b;
    b = temp;
  }

  return Napi::Number::New(env, static_cast<double>(a));
}

// Initialisation du module
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(
    Napi::String::New(env, "fibonacci"),
    Napi::Function::New(env, Fibonacci)
  );
  return exports;
}

NODE_API_MODULE(native_module, Init)
javascript
// Utilisation depuis JavaScript
const native = require('./build/Release/native_module');

// 10x plus rapide que l'équivalent JavaScript
const result = native.fibonacci(50);

Les modules natifs sont utiles pour les calculs intensifs, l'intégration de librairies C/C++ existantes, ou l'accès à des APIs système.

Question 14 : Expliquez le fonctionnement du V8 Garbage Collector

Comprendre le GC aide à écrire du code qui minimise les pauses et la consommation mémoire.

gc-optimization.jsjavascript
// Le GC V8 utilise deux espaces : Young et Old Generation

// 1. Young Generation : objets à courte durée de vie
function shortLivedObjects() {
  for (let i = 0; i < 1000; i++) {
    const temp = { data: i }; // Alloué puis collecté rapidement
  }
  // GC mineur (Scavenge) très rapide
}

// 2. Old Generation : objets qui survivent plusieurs GC
const cache = new Map(); // Survit, promu en Old Generation

// ❌ Pattern problématique : beaucoup d'objets promus
function createManyLongLived() {
  const objects = [];
  for (let i = 0; i < 100000; i++) {
    objects.push({ id: i, data: new Array(100).fill(0) });
  }
  return objects; // Tous promus en Old Gen = GC majeur lent
}

// ✅ Pattern optimisé : réutilisation d'objets
class ObjectPool {
  constructor(factory, size = 100) {
    this.pool = Array.from({ length: size }, factory);
    this.available = [...this.pool];
  }

  acquire() {
    return this.available.pop() || this.pool[0];
  }

  release(obj) {
    // Reset et retour au pool
    Object.keys(obj).forEach(k => obj[k] = null);
    this.available.push(obj);
  }
}

// Monitoring du GC
const v8 = require('v8');

function getHeapStats() {
  const stats = v8.getHeapStatistics();
  return {
    totalHeap: `${Math.round(stats.total_heap_size / 1024 / 1024)}MB`,
    usedHeap: `${Math.round(stats.used_heap_size / 1024 / 1024)}MB`,
    heapLimit: `${Math.round(stats.heap_size_limit / 1024 / 1024)}MB`,
  };
}

Le flag --max-old-space-size permet d'augmenter la limite de la Old Generation pour les applications gourmandes en mémoire.

Question 15 : Comment implémenter le graceful shutdown ?

Un arrêt gracieux permet de terminer les requêtes en cours et de fermer proprement les connexions avant d'arrêter le serveur.

graceful-shutdown.jsjavascript
const http = require('http');

const server = http.createServer((req, res) => {
  // Simulation d'une requête longue
  setTimeout(() => {
    res.writeHead(200);
    res.end('Done');
  }, 2000);
});

// Tracking des connexions actives
let connections = new Set();

server.on('connection', (conn) => {
  connections.add(conn);
  conn.on('close', () => connections.delete(conn));
});

// Fonction de shutdown gracieux
async function shutdown(signal) {
  console.log(`${signal} received, starting graceful shutdown...`);

  // 1. Arrêter d'accepter de nouvelles connexions
  server.close(() => {
    console.log('HTTP server closed');
  });

  // 2. Fermer les connexions idle
  for (const conn of connections) {
    conn.end();
  }

  // 3. Fermer les connexions DB, queues, etc.
  await Promise.all([
    database.disconnect(),
    redisClient.quit(),
    messageQueue.close(),
  ]);

  // 4. Timeout de sécurité
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);

  console.log('Graceful shutdown completed');
  process.exit(0);
}

// Écoute des signaux de terminaison
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Démarrage du serveur
server.listen(3000, () => {
  console.log('Server running on port 3000');
});

En production avec des conteneurs (Docker, Kubernetes), le graceful shutdown est critique pour les déploiements sans interruption de service.

Prêt à réussir tes entretiens Node.js / NestJS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Questions comportementales

Question 16 : Décrivez un problème de performance que vous avez résolu

Cette question évalue l'expérience pratique. Structurer la réponse avec le format STAR (Situation, Task, Action, Result).

Exemple de réponse structurée :

text
Situation : Une API de reporting générait des timeouts sur les requêtes
            dépassant 100 000 enregistrements.

Task :      Réduire le temps de réponse de 45s à moins de 5s.

Action :
1. Profiling avec clinic.js → identifié la sérialisation JSON comme bottleneck
2. Implémentation du streaming avec Transform streams
3. Pagination côté base de données
4. Ajout de cache Redis pour les requêtes fréquentes

Result :    Temps de réponse réduit à 2s, utilisation mémoire divisée par 10.

Question 17 : Comment gérez-vous les dépendances et leurs mises à jour ?

package.json - Bonnes pratiques de versioningjavascript
{
  "dependencies": {
    // ✅ Versions exactes pour la production
    "express": "4.18.2",

    // ✅ Caret pour les mises à jour mineures compatibles
    "lodash": "^4.17.21",

    // ❌ Éviter latest ou *
    // "some-lib": "*"
  },
  "devDependencies": {
    // Outils de qualité
    "npm-check-updates": "^16.0.0"
  },
  "scripts": {
    // Vérification des vulnérabilités
    "audit": "npm audit --audit-level=moderate",

    // Mise à jour interactive
    "update:check": "ncu",
    "update:apply": "ncu -u && npm install"
  },
  "engines": {
    // Spécifier la version Node.js requise
    "node": ">=20.0.0"
  }
}

Mentionner l'utilisation de package-lock.json, Dependabot ou Renovate pour l'automatisation, et les tests de régression avant chaque mise à jour majeure.

Conclusion

Les entretiens Node.js backend évaluent à la fois la compréhension théorique des mécanismes internes et la capacité à résoudre des problèmes pratiques de production. Maîtriser l'Event Loop, les patterns asynchrones, et les techniques d'optimisation constitue le socle attendu pour les postes de développeur backend confirmé.

Checklist de préparation

  • ✅ Comprendre le fonctionnement de l'Event Loop et ses phases
  • ✅ Maîtriser les différences entre callbacks, Promises et async/await
  • ✅ Connaître les patterns de gestion d'erreurs asynchrones
  • ✅ Savoir quand utiliser Streams vs méthodes classiques
  • ✅ Identifier et résoudre les fuites mémoire
  • ✅ Appliquer les bonnes pratiques de sécurité OWASP
  • ✅ Implémenter le clustering et le graceful shutdown
  • ✅ Utiliser les outils de profiling (clinic.js, Chrome DevTools)

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

La préparation technique doit être complétée par des projets pratiques. Construire une API en production, contribuer à des projets open source Node.js, ou résoudre des challenges sur des plateformes comme LeetCode permet de consolider ces connaissances.

Tags

#nodejs
#interview
#backend
#javascript
#entretien technique

Partager

Articles similaires