E-NO Logo
EN FR
Node.js architecture 9 Min Read

Architecture Node.js expliquée avec des exemples pratiques

calendar_today Published: 2026-07-09
update Last Updated: 2026-07-09
analytics SEO Efficiency: 100%
Technical guide illustration for Architecture Node.js expliquée avec des exemples pratiques.

Introduction

Node.js peut sembler trompeusement simple : un thread, beaucoup de vitesse. En réalité, il s'agit d'un runtime bien défini avec des règles claires. Ce guide présente l'architecture Node.js, montre comment les données et le contrôle circulent dans une application, et met en évidence les points de défaillance ainsi que les compromis opérationnels. Vous repartirez avec un schéma fonctionnel complet utilisant Express pour le HTTP, MongoDB pour le stockage, Redis pour le cache, Docker pour le packaging, et un exemple minimal GitLab CI/CD. Les mots-clés pratiques comme Node.js architecture, Node.js components, Node.js data flow, Node.js design et Node.js operations sont abordés naturellement.

Fondamentaux d'architecture

À haut niveau, Node.js c'est :

  • Exécution JavaScript mono-thread sur V8.
  • I/O pilotée par événements via libuv, avec un petit pool de threads pour le système de fichiers, le DNS, la crypto et certaines compressions.
  • Microtâches de Promises exécutées entre les tours de l'event loop.
  • Streams et backpressure en première classe pour un I/O efficace.
  • Worker Threads pour isoler les tâches CPU-bound.
  • Plusieurs processus ou réplicas de conteneurs pour la mise à l'échelle horizontale.

Éléments clés du runtime :

  • V8 : compile et optimise le JS.
  • libuv : boucle d'événements et phases timers, poll, check, close; gère aussi un pool de threads pour les appels bloquants.
  • Event loop : planifie les callbacks quand l'I/O se termine. Gardez-la libre de traitements JS longs.
  • Microtasks : déclenchées par les Promises; elles s'exécutent avant le prochain macrotask turn. Évitez d'affamer la boucle avec des chaînes massives de microtâches.
  • APIs Node : http, net, tls, fs, stream, worker_threads, cluster, process, buffer.

Règles pratiques :

  • Ne bloquez jamais l'event loop avec du CPU lourd; utilisez worker_threads ou un service séparé.
  • Posez des timeouts sur chaque appel sortant (HTTP, DB, cache) et côté serveur.
  • Utilisez streams et backpressure pour les gros volumes.
  • Journalisez en JSON structuré et exposez des métriques.

Flux de données et de contrôle

Chemin type d'une requête dans un service Node.js bien structuré :

  1. Entrée : client -> reverse proxy (ex. Nginx) -> processus Node.
  2. Routage : Express associe une route et invoque un contrôleur.
  3. Contrôleur : valide les entrées, appelle un service, mappe les erreurs vers HTTP.
  4. Service : applique les règles métier, compose dépôts et caches.
  5. Dépôt (repository) : exécute les requêtes DB et renvoie des objets simples.
  6. Cache : court-circuite certaines lectures et protège la DB.
  7. Réponse : le contrôleur renvoie le résultat (souvent JSON); pour de gros flux, utilisez des streams.

Le contrôle est asynchrone. L'I/O ne bloque pas le JS; callbacks ou await reprennent quand prêt. Les travaux CPU-bound doivent être déportés pour maintenir une latence stable.

Exemple d'architecture pratique

Nous allons construire une API minimale de lecture d'utilisateur avec mise en cache.

Arborescence :

app/
  src/
    server.js
    routes/userRoutes.js
    controllers/userController.js
    services/userService.js
    repos/userRepo.js
    clients/mongo.js
    clients/redis.js
    middleware/errors.js
    util/http.js
  package.json

server.js :

const http = require('http');
const express = require('express');
const userRoutes = require('./routes/userRoutes');
const { errorHandler } = require('./middleware/errors');

const app = express();
app.use(express.json());
app.get('/healthz', (req, res) => res.json({ ok: true }));
app.use('/v1/users', userRoutes);
app.use(errorHandler);

const server = http.createServer(app);
// Timeouts raisonnables pour éviter slowloris et sockets bloquées
server.keepAliveTimeout = 65000; // ms
server.headersTimeout = 67000;   // ms

const port = process.env.PORT || 3000;
server.listen(port, () => {
  console.log(JSON.stringify({ level: 'info', msg: `listening on :${port}` }));
});

routes/userRoutes.js :

const express = require('express');
const { getUserById } = require('../controllers/userController');

const router = express.Router();
router.get('/:id', getUserById);
module.exports = router;

controllers/userController.js :

const { findUser } = require('../services/userService');

async function getUserById(req, res, next) {
  try {
    const id = req.params.id;
    if (!id || id.length < 3) {
      return res.status(400).json({ error: 'invalid id' });
    }
    const user = await findUser(id);
    if (!user) return res.status(404).json({ error: 'not found' });
    return res.json(user);
  } catch (err) {
    return next(err);
  }
}

module.exports = { getUserById };

services/userService.js :

const { getUserById } = require('../repos/userRepo');
const cache = require('../clients/redis');

const USER_TTL_SECONDS = 60;

async function findUser(id) {
  const cacheKey = `user:${id}`;
  const cached = await cache.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await getUserById(id);
  if (user) {
    await cache.set(cacheKey, JSON.stringify(user), { EX: USER_TTL_SECONDS });
  }
  return user;
}

module.exports = { findUser };

repos/userRepo.js :

const { getDb } = require('../clients/mongo');

async function getUserById(id) {
  const db = await getDb();
  return db.collection('users').findOne({ _id: id }, { projection: { _id: 1, name: 1, email: 1 } });
}

module.exports = { getUserById };

clients/mongo.js :

const { MongoClient } = require('mongodb');

const uri = process.env.MONGO_URI || 'mongodb://localhost:27017/app';
const client = new MongoClient(uri, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 3000
});
let db;

async function getDb() {
  if (!db) {
    await client.connect();
    db = client.db();
  }
  return db;
}

module.exports = { getDb };

clients/redis.js :

const { createClient } = require('redis');
const client = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });

client.on('error', (err) => {
  console.log(JSON.stringify({ level: 'error', msg: 'redis error', err: String(err) }));
});

let initPromise;
async function ensureReady() {
  if (!initPromise) initPromise = client.connect();
  return initPromise;
}

module.exports = {
  async get(key) { await ensureReady(); return client.get(key); },
  async set(key, value, opts) { await ensureReady(); return client.set(key, value, opts); }
};

middleware/errors.js :

function errorHandler(err, req, res, next) { // eslint-disable-line
  const status = err.statusCode || 500;
  const payload = { error: status === 500 ? 'internal error' : err.message };
  console.log(JSON.stringify({ level: 'error', msg: 'request error', status, err: String(err) }));
  res.status(status).json(payload);
}

module.exports = { errorHandler };

Délester le CPU

Si vous devez effectuer des tâches CPU lourdes, utilisez worker_threads :

// workers/hash.js
const { parentPort, workerData } = require('worker_threads');
const crypto = require('crypto');
const result = crypto.pbkdf2Sync(workerData.pwd, 'salt', 100000, 64, 'sha512').toString('hex');
parentPort.postMessage(result);
// dans un contrôleur ou service
const { Worker } = require('worker_threads');
function hashPassword(pwd) {
  return new Promise((resolve, reject) => {
    const w = new Worker(require.resolve('./workers/hash'), { workerData: { pwd } });
    w.once('message', resolve);
    w.once('error', reject);
    w.once('exit', (code) => { if (code !== 0) reject(new Error('worker exit ' + code)); });
  });
}

Workflow de mise en œuvre

Un workflow clair réduit le rework et aligne le code avec l'exploitation :

  1. Modéliser l'API et les frontières de données. Définir contrats de routes et formes d'erreurs.
  2. Implémenter des couches avec de petites fonctions testables. Garder les contrôleurs fins.
  3. Ajouter les garde-fous d'abord : timeouts serveur et clients, validation d'entrée, logs JSON.
  4. Ajouter un cache quand les lectures dominent. TTLs conservateurs; valider les clés.
  5. Conteneuriser. Image de base légère et utilisateur non-root.
  6. Automatiser tests et builds avec GitLab CI/CD.
  7. Déployer avec plusieurs réplicas derrière un reverse proxy. Readiness probes.
  8. Observer et ajuster : surveiller latence p95, saturation, taux d'erreurs, mémoire.

Exemple Dockerfile :

# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-alpine AS runner
ENV NODE_ENV=production
WORKDIR /app
RUN addgroup -S nodejs && adduser -S node -G nodejs
COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]

.gitlab-ci.yml minimal :

stages: [test, build]

node: test:
  image: node:20-alpine
  stage: test
  script:
    - npm ci
    - npm test -- --ci

docker: build:
  image: gcr.io/kaniko-project/executor: latest
  stage: build
  script:
    - /kaniko/executor --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" --context "$CI_PROJECT_DIR" --dockerfile Dockerfile
  only:
    - main

Plan pilote local

Commencez petit pour mesurer et itérer rapidement.

Périmètre :

  • Un endpoint : GET /v1/users/:id
  • Données dans la collection MongoDB users
  • Cache Redis avec TTL 60 s
  • Health check : GET /healthz

Environnement avec Docker Compose :

version: '3.9'
services:
  app:
    build: .
    ports: ["3000:3000"]
    environment:
      - MONGO_URI=mongodb://mongo:27017/app
      - REDIS_URL=redis://redis:6379
    depends_on: [mongo, redis]
  mongo:
    image: mongo:7
    ports: ["27017:27017"]
  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

Données de seed (à exécuter une fois) :

mongo <<'EOF'
use app
db.users.insertOne({ _id: 'u1', name: 'Ada', email: '[email protected]' })
EOF

Critères d'acceptation :

  • Correctness : 200 avec l'utilisateur pour un id valide, 404 sinon.
  • Latence : p95 < 50 ms pour lectures mises en cache en local, < 150 ms pour lectures à froid.
  • Stabilité : aucun unhandledRejection, aucun uncaughtException.
  • Ressources : mémoire stable sous faible charge.

Mesure avec autocannon :

npx autocannon -c 50 -d 20 http://localhost:3000/v1/users/u1

Inspectez le taux de hit cache en journalisant quand une lecture/écriture de cache aboutit. Augmentez le TTL si la DB est sous pression et que les données sont stables; réduisez-le si la fraîcheur prime.

Opérations et compromis

Points de défaillance courants et mitigations :

  • Blocage de l'event loop : évitez la crypto sync, zlib sync ou le JSON massif sur les chemins chauds. Déportez avec worker_threads. Utilisez setImmediate dans les longues boucles pour céder la main.
  • Timeouts manquants : posez headersTimeout et keepAliveTimeout côté serveur; timeouts clients sur Mongo et Redis.
  • Concurrence non bornée : contrôlez avec p-limit ou mettez en file d'attente.
  • Backpressure : utilisez stream.pipeline pour uploads/downloads; évitez de bufferiser tout en mémoire.
  • Stampede de cache : coalescer les requêtes et utilisez des TTLs avec jitter.
  • Gestion d'erreurs : convertissez les erreurs métier en 4xx; traitez l'inattendu en 5xx. Loggez avec correlation ids si disponibles.
  • Arrêt : captez SIGTERM, cessez d'accepter de nouvelles connexions et attendez la vidange des requêtes en cours.

Compromis :

  • Processus unique vs réplicas : les réplicas apportent résilience et parallélisme. Préférez des conteneurs ou plusieurs processus derrière un proxy plutôt que cluster in-process si vous orchestrez déjà des conteneurs.
  • Worker threads vs services séparés : les threads réduisent la latence pour de courtes tâches CPU; des services séparés isolent les pannes et permettent une mise à l'échelle indépendante pour des charges lourdes.
  • Stratégie de cache : read-through est simple; write-through ou write-behind améliorent respectivement cohérence ou débit selon le besoin.

Checklist rapide :

  • [ ] Timeouts côté serveur et tous les clients
  • [ ] Endpoints health, readiness, liveness
  • [ ] Logs structurés et métriques de base
  • [ ] Backpressure sur les streams
  • [ ] Gestion d'arrêt sûre

Conclusion

Vous disposez maintenant d'un plan d'action concret pour l'architecture Node.js :

  • Comprendre l'event loop et la garder libre de travaux CPU lourds.
  • Structurer en routes, contrôleurs, services, dépôts et clients.
  • Poser timeouts, logs et cache dès le départ.
  • Packager avec Docker et automatiser avec GitLab CI/CD.
  • Démarrer par un pilote local étroit, mesurer la p95 et l'usage de ressources, puis itérer.

Adoptez ces patterns de manière incrémentale. Séparez les responsabilités pour réduire le rework, construisez le pilote minimal d'abord et n'étendez que lorsque les mesures le justifient.

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL