Construire des Microservices Node.js avec Docker, RabbitMQ et API Gateway

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

L'architecture microservices propulse Netflix, Uber et Amazon — et vous pouvez construire les mêmes patterns avec Node.js. Dans ce tutoriel, vous allez construire un backend e-commerce complet avec trois services indépendants communiquant via RabbitMQ, orchestrés par un API Gateway et conteneurisés avec Docker Compose.

Ce que vous allez construire

Une mini plateforme e-commerce composée de microservices indépendants :

  • API Gateway — point d'entrée unique routant les requêtes vers les services, gérant l'authentification et la limitation de débit
  • Service Utilisateur — inscription, authentification, gestion des tokens JWT
  • Service Produit — opérations CRUD sur le catalogue de produits
  • Service Commande — création de commandes avec validation asynchrone du stock via RabbitMQ
  • RabbitMQ — broker de messages pour la communication asynchrone entre services
  • Docker Compose — orchestration de tous les services avec une seule commande

Vue d'ensemble de l'architecture

┌─────────────┐
│   Client     │
└──────┬───────┘
       │
┌──────▼───────┐
│  API Gateway │ :3000
│  (Express)   │
└──┬───┬───┬───┘
   │   │   │
   │   │   └──────────────────┐
   │   │                      │
┌──▼───┴──┐  ┌──────────┐  ┌─▼──────────┐
│  User   │  │ Product  │  │   Order    │
│ Service │  │ Service  │  │  Service   │
│  :3001  │  │  :3002   │  │   :3003    │
└─────────┘  └────┬─────┘  └─────┬──────┘
                  │              │
                  └──────┬───────┘
                         │
                  ┌──────▼───────┐
                  │  RabbitMQ    │
                  │   :5672      │
                  └──────────────┘

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé
  • Docker et Docker Compose installés
  • Compréhension de base d'Express.js et des API REST
  • Familiarité avec les patterns async/await en JavaScript
  • Un éditeur de code (VS Code recommandé)
  • Un terminal

Tous les services tourneront dans des conteneurs Docker. Vous n'avez pas besoin d'installer RabbitMQ localement — Docker gère tout.


Étape 1 : Configuration de la structure du projet

Créez la structure monorepo pour tous les services :

mkdir ecommerce-microservices && cd ecommerce-microservices
 
# Créer les répertoires des services
mkdir -p api-gateway/src
mkdir -p user-service/src
mkdir -p product-service/src
mkdir -p order-service/src
mkdir -p shared/src

Initialisez chaque service avec son propre package.json :

# package.json racine pour la gestion du workspace
cat > package.json << 'EOF'
{
  "name": "ecommerce-microservices",
  "private": true,
  "workspaces": ["api-gateway", "user-service", "product-service", "order-service", "shared"]
}
EOF
 
# Initialiser chaque service
for service in api-gateway user-service product-service order-service shared; do
  cd $service
  npm init -y
  cd ..
done

Votre structure de répertoires devrait ressembler à ceci :

ecommerce-microservices/
├── api-gateway/
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── user-service/
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── product-service/
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── order-service/
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── shared/
│   └── src/
├── docker-compose.yml
└── package.json

Étape 2 : Utilitaires partagés

Créez des modules partagés que tous les services utiliseront. Cela évite la duplication de code entre les services.

Helper de connexion RabbitMQ

// shared/src/rabbitmq.js
const amqp = require("amqplib");
 
class RabbitMQClient {
  constructor() {
    this.connection = null;
    this.channel = null;
  }
 
  async connect(url = process.env.RABBITMQ_URL || "amqp://localhost:5672") {
    const maxRetries = 10;
    let retries = 0;
 
    while (retries < maxRetries) {
      try {
        this.connection = await amqp.connect(url);
        this.channel = await this.connection.createChannel();
        console.log("Connecté à RabbitMQ");
        return this.channel;
      } catch (error) {
        retries++;
        console.log(
          `Tentative de connexion RabbitMQ ${retries}/${maxRetries} échouée. Nouvelle tentative dans 3s...`
        );
        await new Promise((resolve) => setTimeout(resolve, 3000));
      }
    }
    throw new Error("Échec de connexion à RabbitMQ après le maximum de tentatives");
  }
 
  async publishToQueue(queue, message) {
    if (!this.channel) throw new Error("Channel non initialisé");
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
      persistent: true,
    });
  }
 
  async consumeFromQueue(queue, callback) {
    if (!this.channel) throw new Error("Channel non initialisé");
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.prefetch(1);
    this.channel.consume(queue, async (msg) => {
      if (msg) {
        const content = JSON.parse(msg.content.toString());
        try {
          await callback(content);
          this.channel.ack(msg);
        } catch (error) {
          console.error("Échec du traitement du message :", error);
          this.channel.nack(msg, false, true);
        }
      }
    });
    console.log(`Consommation depuis la file : ${queue}`);
  }
 
  async close() {
    if (this.channel) await this.channel.close();
    if (this.connection) await this.connection.close();
  }
}
 
module.exports = { RabbitMQClient };

Helper de réponse partagé

// shared/src/response.js
function successResponse(res, data, statusCode = 200) {
  return res.status(statusCode).json({
    success: true,
    data,
    timestamp: new Date().toISOString(),
  });
}
 
function errorResponse(res, message, statusCode = 500) {
  return res.status(statusCode).json({
    success: false,
    error: message,
    timestamp: new Date().toISOString(),
  });
}
 
module.exports = { successResponse, errorResponse };

Étape 3 : Service Utilisateur

Le Service Utilisateur gère l'inscription et l'authentification avec des tokens JWT.

Installation des dépendances

cd user-service
npm install express jsonwebtoken bcryptjs uuid cors
cd ..

Implémentation du Service Utilisateur

// user-service/src/index.js
const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const { v4: uuidv4 } = require("uuid");
const cors = require("cors");
 
const app = express();
app.use(express.json());
app.use(cors());
 
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const PORT = process.env.PORT || 3001;
 
// Stockage en mémoire (remplacer par une vraie BDD en production)
const users = new Map();
 
// Vérification de santé
app.get("/health", (req, res) => {
  res.json({ status: "healthy", service: "user-service" });
});
 
// Inscription
app.post("/users/register", async (req, res) => {
  try {
    const { email, password, name } = req.body;
 
    if (!email || !password || !name) {
      return res.status(400).json({ error: "Tous les champs sont obligatoires" });
    }
 
    // Vérifier si l'utilisateur existe
    const existingUser = [...users.values()].find((u) => u.email === email);
    if (existingUser) {
      return res.status(409).json({ error: "Email déjà enregistré" });
    }
 
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = {
      id: uuidv4(),
      email,
      name,
      password: hashedPassword,
      createdAt: new Date().toISOString(),
    };
 
    users.set(user.id, user);
 
    const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
      expiresIn: "24h",
    });
 
    res.status(201).json({
      success: true,
      data: {
        user: { id: user.id, email: user.email, name: user.name },
        token,
      },
    });
  } catch (error) {
    res.status(500).json({ error: "Échec de l'inscription" });
  }
});
 
// Connexion
app.post("/users/login", async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = [...users.values()].find((u) => u.email === email);
 
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: "Identifiants invalides" });
    }
 
    const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
      expiresIn: "24h",
    });
 
    res.json({
      success: true,
      data: {
        user: { id: user.id, email: user.email, name: user.name },
        token,
      },
    });
  } catch (error) {
    res.status(500).json({ error: "Échec de la connexion" });
  }
});
 
// Valider le token (utilisé en interne par l'API Gateway)
app.get("/users/validate", (req, res) => {
  try {
    const token = req.headers.authorization?.split(" ")[1];
    if (!token) return res.status(401).json({ error: "Aucun token fourni" });
 
    const decoded = jwt.verify(token, JWT_SECRET);
    const user = users.get(decoded.userId);
 
    if (!user) return res.status(404).json({ error: "Utilisateur non trouvé" });
 
    res.json({
      success: true,
      data: { id: user.id, email: user.email, name: user.name },
    });
  } catch (error) {
    res.status(401).json({ error: "Token invalide" });
  }
});
 
// Obtenir le profil utilisateur
app.get("/users/:id", (req, res) => {
  const user = users.get(req.params.id);
  if (!user) return res.status(404).json({ error: "Utilisateur non trouvé" });
 
  res.json({
    success: true,
    data: { id: user.id, email: user.email, name: user.name },
  });
});
 
app.listen(PORT, () => {
  console.log(`Service Utilisateur en écoute sur le port ${PORT}`);
});

Étape 4 : Service Produit

Le Service Produit gère le catalogue de produits et publie des événements d'inventaire vers RabbitMQ.

Installation des dépendances

cd product-service
npm install express uuid cors amqplib
cd ..

Implémentation du Service Produit

// product-service/src/index.js
const express = require("express");
const { v4: uuidv4 } = require("uuid");
const cors = require("cors");
const { RabbitMQClient } = require("../../shared/src/rabbitmq");
 
const app = express();
app.use(express.json());
app.use(cors());
 
const PORT = process.env.PORT || 3002;
const rabbit = new RabbitMQClient();
 
// Stockage des produits en mémoire
const products = new Map();
 
// Données initiales
function seedProducts() {
  const initial = [
    { name: "Clavier sans fil", price: 49.99, stock: 100, category: "electronique" },
    { name: "Hub USB-C", price: 29.99, stock: 200, category: "electronique" },
    { name: "Tapis de bureau ergonomique", price: 39.99, stock: 50, category: "bureau" },
    { name: "Casque anti-bruit", price: 199.99, stock: 75, category: "electronique" },
    { name: "Souris ergonomique", price: 59.99, stock: 150, category: "electronique" },
  ];
 
  initial.forEach((p) => {
    const product = { ...p, id: uuidv4(), createdAt: new Date().toISOString() };
    products.set(product.id, product);
  });
}
 
// Vérification de santé
app.get("/health", (req, res) => {
  res.json({ status: "healthy", service: "product-service" });
});
 
// Lister tous les produits
app.get("/products", (req, res) => {
  const { category, minPrice, maxPrice } = req.query;
  let result = [...products.values()];
 
  if (category) result = result.filter((p) => p.category === category);
  if (minPrice) result = result.filter((p) => p.price >= parseFloat(minPrice));
  if (maxPrice) result = result.filter((p) => p.price <= parseFloat(maxPrice));
 
  res.json({ success: true, data: result, total: result.length });
});
 
// Obtenir un produit
app.get("/products/:id", (req, res) => {
  const product = products.get(req.params.id);
  if (!product) return res.status(404).json({ error: "Produit non trouvé" });
  res.json({ success: true, data: product });
});
 
// Créer un produit
app.post("/products", (req, res) => {
  const { name, price, stock, category } = req.body;
 
  if (!name || !price || stock === undefined || !category) {
    return res.status(400).json({ error: "Tous les champs sont obligatoires" });
  }
 
  const product = {
    id: uuidv4(),
    name,
    price: parseFloat(price),
    stock: parseInt(stock),
    category,
    createdAt: new Date().toISOString(),
  };
 
  products.set(product.id, product);
  res.status(201).json({ success: true, data: product });
});
 
// Mettre à jour le stock (appelé quand une commande est confirmée)
app.patch("/products/:id/stock", async (req, res) => {
  const product = products.get(req.params.id);
  if (!product) return res.status(404).json({ error: "Produit non trouvé" });
 
  const { quantity } = req.body;
  product.stock += quantity; // négatif pour diminuer
 
  // Publier l'événement de mise à jour du stock
  await rabbit.publishToQueue("stock_updated", {
    productId: product.id,
    newStock: product.stock,
    change: quantity,
    timestamp: new Date().toISOString(),
  });
 
  res.json({ success: true, data: product });
});
 
// Vérifier la disponibilité du stock (endpoint interne)
app.post("/products/check-stock", (req, res) => {
  const { items } = req.body; // [{ productId, quantity }]
  const results = items.map((item) => {
    const product = products.get(item.productId);
    return {
      productId: item.productId,
      requested: item.quantity,
      available: product ? product.stock : 0,
      sufficient: product ? product.stock >= item.quantity : false,
    };
  });
 
  const allAvailable = results.every((r) => r.sufficient);
  res.json({ success: true, data: { allAvailable, items: results } });
});
 
async function start() {
  seedProducts();
  await rabbit.connect(process.env.RABBITMQ_URL);
  app.listen(PORT, () => {
    console.log(`Service Produit en écoute sur le port ${PORT}`);
  });
}
 
start().catch(console.error);

Étape 5 : Service Commande avec messagerie asynchrone

Le Service Commande est le plus intéressant — il crée des commandes et utilise RabbitMQ pour valider de manière asynchrone le stock et traiter les commandes.

Installation des dépendances

cd order-service
npm install express uuid cors amqplib node-fetch
cd ..

Implémentation du Service Commande

// order-service/src/index.js
const express = require("express");
const { v4: uuidv4 } = require("uuid");
const cors = require("cors");
const { RabbitMQClient } = require("../../shared/src/rabbitmq");
 
const app = express();
app.use(express.json());
app.use(cors());
 
const PORT = process.env.PORT || 3003;
const PRODUCT_SERVICE_URL =
  process.env.PRODUCT_SERVICE_URL || "http://localhost:3002";
const rabbit = new RabbitMQClient();
 
// Stockage des commandes en mémoire
const orders = new Map();
 
// Vérification de santé
app.get("/health", (req, res) => {
  res.json({ status: "healthy", service: "order-service" });
});
 
// Créer une commande
app.post("/orders", async (req, res) => {
  try {
    const { userId, items } = req.body;
    // items: [{ productId, quantity, price }]
 
    if (!userId || !items || items.length === 0) {
      return res.status(400).json({ error: "userId et items sont obligatoires" });
    }
 
    const order = {
      id: uuidv4(),
      userId,
      items,
      total: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
      status: "pending",
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };
 
    orders.set(order.id, order);
 
    // Publier la commande dans RabbitMQ pour traitement asynchrone
    await rabbit.publishToQueue("order_created", {
      orderId: order.id,
      userId: order.userId,
      items: order.items,
      total: order.total,
    });
 
    console.log(`Commande ${order.id} créée et envoyée dans la file`);
 
    res.status(201).json({
      success: true,
      data: order,
      message: "Commande créée. Validation du stock en cours...",
    });
  } catch (error) {
    console.error("Échec de la création de commande :", error);
    res.status(500).json({ error: "Échec de la création de commande" });
  }
});
 
// Obtenir une commande par ID
app.get("/orders/:id", (req, res) => {
  const order = orders.get(req.params.id);
  if (!order) return res.status(404).json({ error: "Commande non trouvée" });
  res.json({ success: true, data: order });
});
 
// Obtenir les commandes d'un utilisateur
app.get("/orders/user/:userId", (req, res) => {
  const userOrders = [...orders.values()].filter(
    (o) => o.userId === req.params.userId
  );
  res.json({ success: true, data: userOrders, total: userOrders.length });
});
 
// Traiter la file de commandes - valider le stock et confirmer/rejeter
async function processOrderQueue() {
  await rabbit.consumeFromQueue("order_created", async (orderData) => {
    console.log(`Traitement de la commande : ${orderData.orderId}`);
    const order = orders.get(orderData.orderId);
    if (!order) return;
 
    try {
      // Vérifier le stock auprès du Service Produit
      const response = await fetch(`${PRODUCT_SERVICE_URL}/products/check-stock`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ items: orderData.items }),
      });
 
      const stockCheck = await response.json();
 
      if (stockCheck.data.allAvailable) {
        // Déduire le stock pour chaque article
        for (const item of orderData.items) {
          await fetch(
            `${PRODUCT_SERVICE_URL}/products/${item.productId}/stock`,
            {
              method: "PATCH",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ quantity: -item.quantity }),
            }
          );
        }
 
        order.status = "confirmed";
        order.updatedAt = new Date().toISOString();
 
        // Publier l'événement de confirmation
        await rabbit.publishToQueue("order_confirmed", {
          orderId: order.id,
          userId: order.userId,
          total: order.total,
        });
 
        console.log(`Commande ${order.id} confirmée`);
      } else {
        order.status = "rejected";
        order.reason = "Stock insuffisant";
        order.updatedAt = new Date().toISOString();
 
        await rabbit.publishToQueue("order_rejected", {
          orderId: order.id,
          userId: order.userId,
          reason: "Stock insuffisant",
          details: stockCheck.data.items,
        });
 
        console.log(`Commande ${order.id} rejetée - stock insuffisant`);
      }
    } catch (error) {
      console.error(`Échec du traitement de la commande ${orderData.orderId} :`, error);
      order.status = "failed";
      order.reason = "Erreur de traitement";
      order.updatedAt = new Date().toISOString();
    }
  });
}
 
async function start() {
  await rabbit.connect(process.env.RABBITMQ_URL);
  await processOrderQueue();
  app.listen(PORT, () => {
    console.log(`Service Commande en écoute sur le port ${PORT}`);
  });
}
 
start().catch(console.error);

En production, vous utiliseriez une vraie base de données (PostgreSQL, MongoDB) au lieu de Maps en mémoire. L'approche en mémoire est utilisée ici pour se concentrer sur les patterns d'architecture microservices sans surcharge de configuration de base de données.


Étape 6 : API Gateway

L'API Gateway est le point d'entrée unique pour toutes les requêtes client. Il route le trafic, valide les tokens JWT et applique la limitation de débit.

Installation des dépendances

cd api-gateway
npm install express http-proxy-middleware cors express-rate-limit jsonwebtoken
cd ..

Implémentation de l'API Gateway

// api-gateway/src/index.js
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const cors = require("cors");
const rateLimit = require("express-rate-limit");
const jwt = require("jsonwebtoken");
 
const app = express();
app.use(cors());
app.use(express.json());
 
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
 
// URLs des services
const SERVICES = {
  users: process.env.USER_SERVICE_URL || "http://localhost:3001",
  products: process.env.PRODUCT_SERVICE_URL || "http://localhost:3002",
  orders: process.env.ORDER_SERVICE_URL || "http://localhost:3003",
};
 
// Limitation de débit
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  message: { error: "Trop de requêtes, veuillez réessayer plus tard" },
});
 
app.use(limiter);
 
// Middleware d'authentification JWT
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
 
  if (!token) {
    return res.status(401).json({ error: "Authentification requise" });
  }
 
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: "Token invalide ou expiré" });
  }
}
 
// Middleware de journalisation des requêtes
app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(
      `${req.method} ${req.originalUrl} -> ${res.statusCode} (${duration}ms)`
    );
  });
  next();
});
 
// Vérification de santé de tous les services
app.get("/health", async (req, res) => {
  const checks = {};
 
  for (const [name, url] of Object.entries(SERVICES)) {
    try {
      const response = await fetch(`${url}/health`);
      const data = await response.json();
      checks[name] = { status: "up", ...data };
    } catch {
      checks[name] = { status: "down" };
    }
  }
 
  const allUp = Object.values(checks).every((c) => c.status === "up");
  res.status(allUp ? 200 : 503).json({
    gateway: "healthy",
    services: checks,
    timestamp: new Date().toISOString(),
  });
});
 
// Routes publiques (pas d'authentification requise)
app.use(
  "/api/users/register",
  createProxyMiddleware({
    target: SERVICES.users,
    changeOrigin: true,
    pathRewrite: { "^/api/users/register": "/users/register" },
  })
);
 
app.use(
  "/api/users/login",
  createProxyMiddleware({
    target: SERVICES.users,
    changeOrigin: true,
    pathRewrite: { "^/api/users/login": "/users/login" },
  })
);
 
app.use(
  "/api/products",
  createProxyMiddleware({
    target: SERVICES.products,
    changeOrigin: true,
    pathRewrite: { "^/api/products": "/products" },
  })
);
 
// Routes protégées (authentification requise)
app.use(
  "/api/orders",
  authenticate,
  createProxyMiddleware({
    target: SERVICES.orders,
    changeOrigin: true,
    pathRewrite: { "^/api/orders": "/orders" },
    onProxyReq(proxyReq, req) {
      // Transmettre les infos utilisateur au service en aval
      proxyReq.setHeader("X-User-Id", req.user.userId);
      proxyReq.setHeader("X-User-Email", req.user.email);
    },
  })
);
 
app.use(
  "/api/users",
  authenticate,
  createProxyMiddleware({
    target: SERVICES.users,
    changeOrigin: true,
    pathRewrite: { "^/api/users": "/users" },
  })
);
 
// Gestionnaire 404
app.use((req, res) => {
  res.status(404).json({ error: "Route non trouvée" });
});
 
// Gestionnaire d'erreurs
app.use((err, req, res, next) => {
  console.error("Erreur du gateway :", err.message);
  res.status(500).json({ error: "Erreur interne du gateway" });
});
 
app.listen(PORT, () => {
  console.log(`API Gateway en écoute sur le port ${PORT}`);
  console.log("Routes des services :");
  Object.entries(SERVICES).forEach(([name, url]) => {
    console.log(`  /api/${name} -> ${url}`);
  });
});

Étape 7 : Conteneuriser le tout

Dockerfile des services

Créez le même Dockerfile pour chaque service (api-gateway, user-service, product-service, order-service) :

# api-gateway/Dockerfile (même pattern pour tous les services)
FROM node:20-alpine
 
WORKDIR /app
 
# Copier le module partagé
COPY shared/ ./shared/
 
# Copier les fichiers du service
COPY api-gateway/package*.json ./
RUN npm install --production
 
COPY api-gateway/src/ ./src/
 
EXPOSE 3000
 
CMD ["node", "src/index.js"]

Adaptez les chemins pour chaque service :

# user-service/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY shared/ ./shared/
COPY user-service/package*.json ./
RUN npm install --production
COPY user-service/src/ ./src/
EXPOSE 3001
CMD ["node", "src/index.js"]
# product-service/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY shared/ ./shared/
COPY product-service/package*.json ./
RUN npm install --production
COPY product-service/src/ ./src/
EXPOSE 3002
CMD ["node", "src/index.js"]
# order-service/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY shared/ ./shared/
COPY order-service/package*.json ./
RUN npm install --production
COPY order-service/src/ ./src/
EXPOSE 3003
CMD ["node", "src/index.js"]

Configuration Docker Compose

# docker-compose.yml
version: "3.8"
 
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"  # Interface de gestion
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: rabbitmq123
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  user-service:
    build:
      context: .
      dockerfile: user-service/Dockerfile
    ports:
      - "3001:3001"
    environment:
      PORT: 3001
      JWT_SECRET: ma-cle-jwt-super-secrete-a-changer-en-production
    restart: unless-stopped
 
  product-service:
    build:
      context: .
      dockerfile: product-service/Dockerfile
    ports:
      - "3002:3002"
    environment:
      PORT: 3002
      RABBITMQ_URL: amqp://admin:rabbitmq123@rabbitmq:5672
    depends_on:
      rabbitmq:
        condition: service_healthy
    restart: unless-stopped
 
  order-service:
    build:
      context: .
      dockerfile: order-service/Dockerfile
    ports:
      - "3003:3003"
    environment:
      PORT: 3003
      RABBITMQ_URL: amqp://admin:rabbitmq123@rabbitmq:5672
      PRODUCT_SERVICE_URL: http://product-service:3002
    depends_on:
      rabbitmq:
        condition: service_healthy
      product-service:
        condition: service_started
    restart: unless-stopped
 
  api-gateway:
    build:
      context: .
      dockerfile: api-gateway/Dockerfile
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      JWT_SECRET: ma-cle-jwt-super-secrete-a-changer-en-production
      USER_SERVICE_URL: http://user-service:3001
      PRODUCT_SERVICE_URL: http://product-service:3002
      ORDER_SERVICE_URL: http://order-service:3003
    depends_on:
      - user-service
      - product-service
      - order-service
    restart: unless-stopped
 
volumes:
  rabbitmq_data:

Étape 8 : Lancement et tests

Démarrer tous les services

# Construire et démarrer tout
docker compose up --build -d
 
# Vérifier que tous les services tournent
docker compose ps
 
# Voir les logs
docker compose logs -f

Vous devriez voir les cinq conteneurs en cours d'exécution (RabbitMQ + 4 services).

Tester l'API Gateway

Ouvrez un nouveau terminal et testez le flux complet :

# 1. Vérifier la santé du gateway
curl http://localhost:3000/health | jq
 
# 2. Inscrire un utilisateur
curl -X POST http://localhost:3000/api/users/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@exemple.com",
    "password": "motDePasse123",
    "name": "Utilisateur Test"
  }' | jq

Sauvegardez le token de la réponse, puis continuez :

# 3. Parcourir les produits (public - pas d'auth nécessaire)
curl http://localhost:3000/api/products | jq
 
# 4. Créer une commande (nécessite l'auth)
TOKEN="votre-token-jwt-ici"
 
curl -X POST http://localhost:3000/api/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "userId": "votre-id-utilisateur",
    "items": [
      {
        "productId": "id-produit-etape-3",
        "quantity": 2,
        "price": 49.99
      }
    ]
  }' | jq
 
# 5. Vérifier le statut de la commande (devrait passer de "pending" à "confirmed")
curl http://localhost:3000/api/orders/ID_COMMANDE \
  -H "Authorization: Bearer $TOKEN" | jq

Surveiller RabbitMQ

Ouvrez le tableau de bord de gestion RabbitMQ à l'adresse http://localhost:15672 (identifiants : admin / rabbitmq123). Vous verrez :

  • Files d'attente : order_created, order_confirmed, order_rejected, stock_updated
  • Taux de messages : Messages circulant entre les services
  • Connexions : Une connexion par service

Étape 9 : Ajouter la résilience des services

Pattern Circuit Breaker

Ajoutez un circuit breaker simple pour protéger les services contre les défaillances en cascade :

// shared/src/circuit-breaker.js
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
    this.lastFailureTime = null;
  }
 
  async execute(fn) {
    if (this.state === "OPEN") {
      if (Date.now() - this.lastFailureTime > this.resetTimeout) {
        this.state = "HALF_OPEN";
      } else {
        throw new Error("Circuit breaker OUVERT - service indisponible");
      }
    }
 
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
 
  onSuccess() {
    this.failures = 0;
    this.state = "CLOSED";
  }
 
  onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.failureThreshold) {
      this.state = "OPEN";
      console.warn("Circuit breaker OUVERT - trop de défaillances");
    }
  }
 
  getState() {
    return {
      state: this.state,
      failures: this.failures,
      threshold: this.failureThreshold,
    };
  }
}
 
module.exports = { CircuitBreaker };

Utilisez-le dans le Service Commande lors de l'appel au Service Produit :

const { CircuitBreaker } = require("../../shared/src/circuit-breaker");
const productServiceBreaker = new CircuitBreaker({
  failureThreshold: 3,
  resetTimeout: 15000,
});
 
// Dans la fonction de traitement des commandes :
const stockCheck = await productServiceBreaker.execute(async () => {
  const response = await fetch(
    `${PRODUCT_SERVICE_URL}/products/check-stock`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items: orderData.items }),
    }
  );
  if (!response.ok) throw new Error(`Vérification du stock échouée : ${response.status}`);
  return response.json();
});

Étape 10 : Ajouter une journalisation structurée

Une journalisation cohérente à travers les services facilite grandement le débogage :

// shared/src/logger.js
function createLogger(serviceName) {
  function formatLog(level, message, meta = {}) {
    return JSON.stringify({
      timestamp: new Date().toISOString(),
      service: serviceName,
      level,
      message,
      ...meta,
    });
  }
 
  return {
    info: (message, meta) => console.log(formatLog("info", message, meta)),
    warn: (message, meta) => console.warn(formatLog("warn", message, meta)),
    error: (message, meta) => console.error(formatLog("error", message, meta)),
    debug: (message, meta) => console.debug(formatLog("debug", message, meta)),
  };
}
 
module.exports = { createLogger };

Utilisation dans n'importe quel service :

const { createLogger } = require("../../shared/src/logger");
const logger = createLogger("order-service");
 
logger.info("Commande créée", { orderId: order.id, userId: order.userId });
logger.error("Échec du traitement de la commande", { orderId, error: error.message });

Dépannage

Connexion RabbitMQ refusée

Si les services échouent à se connecter à RabbitMQ :

# Vérifier que RabbitMQ tourne
docker compose logs rabbitmq
 
# Redémarrer RabbitMQ
docker compose restart rabbitmq

Le client RabbitMQ partagé inclut une logique de reconnexion (10 tentatives avec des délais de 3 secondes), ce qui gère l'ordre de démarrage.

Un service ne peut pas atteindre un autre service

Dans Docker Compose, les services se contactent par nom de service (par exemple http://product-service:3002), pas par localhost. Vérifiez que vos variables d'environnement sont correctement configurées.

Conflits de ports

Si les ports 3000-3003 ou 5672/15672 sont déjà utilisés :

# Trouver ce qui utilise le port
lsof -i :3000
 
# Ou changer les ports dans docker-compose.yml
ports:
  - "4000:3000"  # Mapper vers un port hôte différent

Prochaines étapes

Maintenant que vous avez une architecture microservices fonctionnelle, voici comment l'étendre :

  • Ajouter une vraie base de données : Remplacer les Maps en mémoire par PostgreSQL avec Prisma ou Drizzle ORM
  • Ajouter un Service de Notifications : Consommer les événements order_confirmed pour envoyer des emails via Resend
  • Implémenter le pattern saga : Pour les transactions multi-étapes nécessitant un rollback
  • Ajouter OpenTelemetry : Traçage distribué à travers les services
  • Déployer en production : Utiliser Kubernetes ou Docker Swarm pour l'orchestration
  • Ajouter un Gateway GraphQL : Remplacer le proxy REST par une couche GraphQL fédérée

Conclusion

Vous avez construit une architecture microservices complète avec :

  • Décomposition en services — chaque domaine (utilisateurs, produits, commandes) a son propre service
  • Messagerie asynchrone — RabbitMQ découple le traitement des commandes de la validation du stock
  • API Gateway — point d'entrée unique avec authentification, limitation de débit et routage des requêtes
  • Circuit breaker — résilience contre les défaillances en cascade
  • Conteneurisation — Docker Compose pour des déploiements reproductibles
  • Journalisation structurée — logs JSON cohérents à travers tous les services

Cette architecture s'adapte horizontalement : vous pouvez exécuter plusieurs instances de n'importe quel service derrière un load balancer, et RabbitMQ distribue automatiquement les messages entre les consommateurs. Les patterns démontrés ici — décomposition en services, messagerie asynchrone, API gateway, circuit breaker — sont les mêmes patterns utilisés en production par des entreprises traitant des millions de requêtes quotidiennes.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 8 Les Bases de Laravel 11 : Vues.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Docker Compose pour développeurs Full-Stack : Next.js, PostgreSQL et Redis

Apprenez à conteneuriser une application Next.js full-stack avec PostgreSQL et Redis en utilisant Docker Compose. Ce tutoriel pratique couvre l'orchestration multi-services, les workflows de développement, le rechargement à chaud, les health checks et les configurations prêtes pour la production.

28 min read·

Construire des API REST avec Go et Fiber : Guide pratique pour débutants

Apprenez à construire des API REST rapides et prêtes pour la production avec Go et le framework Fiber. Ce guide pas à pas couvre la configuration du projet, le routage, le traitement JSON, la connexion à la base de données avec GORM, les middlewares, la gestion des erreurs et les tests — de zéro à une API fonctionnelle.

30 min read·