بناء خدمات مصغرة Node.js مع Docker و RabbitMQ و API Gateway

بنية الخدمات المصغرة تشغّل Netflix و Uber و Amazon — ويمكنك بناء نفس الأنماط باستخدام Node.js. في هذا الدليل، ستبني واجهة خلفية كاملة للتجارة الإلكترونية بثلاث خدمات مستقلة تتواصل عبر RabbitMQ، يديرها API Gateway، ومعبأة في حاويات Docker Compose.
ما ستبنيه
منصة تجارة إلكترونية مصغرة مكونة من خدمات مصغرة مستقلة:
- API Gateway — نقطة دخول واحدة توجه الطلبات للخدمات، تدير المصادقة وتحديد معدل الطلبات
- خدمة المستخدمين — التسجيل، المصادقة، إدارة رموز JWT
- خدمة المنتجات — عمليات CRUD على كتالوج المنتجات
- خدمة الطلبات — إنشاء الطلبات مع التحقق غير المتزامن من المخزون عبر RabbitMQ
- RabbitMQ — وسيط رسائل للتواصل غير المتزامن بين الخدمات
- Docker Compose — تنسيق جميع الخدمات بأمر واحد
نظرة عامة على البنية
┌─────────────┐
│ العميل │
└──────┬───────┘
│
┌──────▼───────┐
│ API Gateway │ :3000
│ (Express) │
└──┬───┬───┬───┘
│ │ │
│ │ └──────────────────┐
│ │ │
┌──▼───┴──┐ ┌──────────┐ ┌─▼──────────┐
│ User │ │ Product │ │ Order │
│ Service │ │ Service │ │ Service │
│ :3001 │ │ :3002 │ │ :3003 │
└─────────┘ └────┬─────┘ └─────┬──────┘
│ │
└──────┬───────┘
│
┌──────▼───────┐
│ RabbitMQ │
│ :5672 │
└──────────────┘
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- Docker و Docker Compose مثبتان
- فهم أساسي لـ Express.js و واجهات REST
- إلمام بأنماط async/await في JavaScript
- محرر أكواد (يُنصح بـ VS Code)
- طرفية (Terminal)
جميع الخدمات ستعمل في حاويات Docker. لا تحتاج لتثبيت RabbitMQ محلياً — Docker يتولى كل شيء.
الخطوة 1: إعداد هيكل المشروع
أنشئ هيكل monorepo لجميع الخدمات:
mkdir ecommerce-microservices && cd ecommerce-microservices
# إنشاء مجلدات الخدمات
mkdir -p api-gateway/src
mkdir -p user-service/src
mkdir -p product-service/src
mkdir -p order-service/src
mkdir -p shared/srcابدأ كل خدمة بملف package.json خاص بها:
# ملف package.json الجذري لإدارة مساحة العمل
cat > package.json << 'EOF'
{
"name": "ecommerce-microservices",
"private": true,
"workspaces": ["api-gateway", "user-service", "product-service", "order-service", "shared"]
}
EOF
# تهيئة كل خدمة
for service in api-gateway user-service product-service order-service shared; do
cd $service
npm init -y
cd ..
doneيجب أن يبدو هيكل المجلدات كالتالي:
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
الخطوة 2: الأدوات المشتركة
أنشئ وحدات مشتركة ستستخدمها جميع الخدمات. هذا يمنع تكرار الكود بين الخدمات.
مساعد اتصال 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("تم الاتصال بـ RabbitMQ");
return this.channel;
} catch (error) {
retries++;
console.log(
`محاولة الاتصال بـ RabbitMQ ${retries}/${maxRetries} فشلت. إعادة المحاولة خلال 3 ثوانٍ...`
);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
throw new Error("فشل الاتصال بـ RabbitMQ بعد الحد الأقصى من المحاولات");
}
async publishToQueue(queue, message) {
if (!this.channel) throw new Error("القناة غير مهيأة");
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("القناة غير مهيأة");
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("فشل معالجة الرسالة:", error);
this.channel.nack(msg, false, true);
}
}
});
console.log(`الاستماع إلى الطابور: ${queue}`);
}
async close() {
if (this.channel) await this.channel.close();
if (this.connection) await this.connection.close();
}
}
module.exports = { RabbitMQClient };مساعد الاستجابة المشترك
// 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 };الخطوة 3: خدمة المستخدمين
خدمة المستخدمين تتولى التسجيل والمصادقة باستخدام رموز JWT.
تثبيت التبعيات
cd user-service
npm install express jsonwebtoken bcryptjs uuid cors
cd ..تنفيذ خدمة المستخدمين
// 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;
// تخزين في الذاكرة (استبدل بقاعدة بيانات حقيقية في الإنتاج)
const users = new Map();
// فحص الصحة
app.get("/health", (req, res) => {
res.json({ status: "healthy", service: "user-service" });
});
// التسجيل
app.post("/users/register", async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password || !name) {
return res.status(400).json({ error: "جميع الحقول مطلوبة" });
}
const existingUser = [...users.values()].find((u) => u.email === email);
if (existingUser) {
return res.status(409).json({ error: "البريد الإلكتروني مسجل مسبقاً" });
}
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: "فشل التسجيل" });
}
});
// تسجيل الدخول
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: "بيانات الاعتماد غير صالحة" });
}
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: "فشل تسجيل الدخول" });
}
});
// التحقق من الرمز (يستخدمه API Gateway داخلياً)
app.get("/users/validate", (req, res) => {
try {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "لم يتم توفير رمز" });
const decoded = jwt.verify(token, JWT_SECRET);
const user = users.get(decoded.userId);
if (!user) return res.status(404).json({ error: "المستخدم غير موجود" });
res.json({
success: true,
data: { id: user.id, email: user.email, name: user.name },
});
} catch (error) {
res.status(401).json({ error: "رمز غير صالح" });
}
});
// الحصول على ملف المستخدم
app.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) return res.status(404).json({ error: "المستخدم غير موجود" });
res.json({
success: true,
data: { id: user.id, email: user.email, name: user.name },
});
});
app.listen(PORT, () => {
console.log(`خدمة المستخدمين تعمل على المنفذ ${PORT}`);
});الخطوة 4: خدمة المنتجات
خدمة المنتجات تدير كتالوج المنتجات وتنشر أحداث المخزون إلى RabbitMQ.
تثبيت التبعيات
cd product-service
npm install express uuid cors amqplib
cd ..تنفيذ خدمة المنتجات
// 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();
// تخزين المنتجات في الذاكرة
const products = new Map();
// البيانات الأولية
function seedProducts() {
const initial = [
{ name: "لوحة مفاتيح لاسلكية", price: 49.99, stock: 100, category: "إلكترونيات" },
{ name: "موزع USB-C", price: 29.99, stock: 200, category: "إلكترونيات" },
{ name: "حصيرة مكتب مريحة", price: 39.99, stock: 50, category: "مكتب" },
{ name: "سماعات عازلة للضوضاء", price: 199.99, stock: 75, category: "إلكترونيات" },
{ name: "فأرة مريحة", price: 59.99, stock: 150, category: "إلكترونيات" },
];
initial.forEach((p) => {
const product = { ...p, id: uuidv4(), createdAt: new Date().toISOString() };
products.set(product.id, product);
});
}
// فحص الصحة
app.get("/health", (req, res) => {
res.json({ status: "healthy", service: "product-service" });
});
// قائمة جميع المنتجات
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 });
});
// الحصول على منتج واحد
app.get("/products/:id", (req, res) => {
const product = products.get(req.params.id);
if (!product) return res.status(404).json({ error: "المنتج غير موجود" });
res.json({ success: true, data: product });
});
// إنشاء منتج
app.post("/products", (req, res) => {
const { name, price, stock, category } = req.body;
if (!name || !price || stock === undefined || !category) {
return res.status(400).json({ error: "جميع الحقول مطلوبة" });
}
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 });
});
// تحديث المخزون (يُستدعى عند تأكيد الطلب)
app.patch("/products/:id/stock", async (req, res) => {
const product = products.get(req.params.id);
if (!product) return res.status(404).json({ error: "المنتج غير موجود" });
const { quantity } = req.body;
product.stock += quantity; // سالب للإنقاص
// نشر حدث تحديث المخزون
await rabbit.publishToQueue("stock_updated", {
productId: product.id,
newStock: product.stock,
change: quantity,
timestamp: new Date().toISOString(),
});
res.json({ success: true, data: product });
});
// التحقق من توفر المخزون (نقطة نهاية داخلية)
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(`خدمة المنتجات تعمل على المنفذ ${PORT}`);
});
}
start().catch(console.error);الخطوة 5: خدمة الطلبات مع المراسلة غير المتزامنة
خدمة الطلبات هي الأكثر إثارة — تنشئ الطلبات وتستخدم RabbitMQ للتحقق بشكل غير متزامن من المخزون ومعالجة الطلبات.
تثبيت التبعيات
cd order-service
npm install express uuid cors amqplib node-fetch
cd ..تنفيذ خدمة الطلبات
// 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();
// تخزين الطلبات في الذاكرة
const orders = new Map();
// فحص الصحة
app.get("/health", (req, res) => {
res.json({ status: "healthy", service: "order-service" });
});
// إنشاء طلب
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 و items مطلوبان" });
}
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);
// نشر الطلب في RabbitMQ للمعالجة غير المتزامنة
await rabbit.publishToQueue("order_created", {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
});
console.log(`الطلب ${order.id} تم إنشاؤه وإرساله إلى الطابور`);
res.status(201).json({
success: true,
data: order,
message: "تم إنشاء الطلب. جارٍ التحقق من المخزون...",
});
} catch (error) {
console.error("فشل إنشاء الطلب:", error);
res.status(500).json({ error: "فشل إنشاء الطلب" });
}
});
// الحصول على طلب بالمعرف
app.get("/orders/:id", (req, res) => {
const order = orders.get(req.params.id);
if (!order) return res.status(404).json({ error: "الطلب غير موجود" });
res.json({ success: true, data: order });
});
// الحصول على طلبات المستخدم
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 });
});
// معالجة طابور الطلبات - التحقق من المخزون والتأكيد/الرفض
async function processOrderQueue() {
await rabbit.consumeFromQueue("order_created", async (orderData) => {
console.log(`معالجة الطلب: ${orderData.orderId}`);
const order = orders.get(orderData.orderId);
if (!order) return;
try {
// التحقق من المخزون مع خدمة المنتجات
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) {
// خصم المخزون لكل عنصر
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();
// نشر حدث التأكيد
await rabbit.publishToQueue("order_confirmed", {
orderId: order.id,
userId: order.userId,
total: order.total,
});
console.log(`الطلب ${order.id} تم تأكيده`);
} else {
order.status = "rejected";
order.reason = "مخزون غير كافٍ";
order.updatedAt = new Date().toISOString();
await rabbit.publishToQueue("order_rejected", {
orderId: order.id,
userId: order.userId,
reason: "مخزون غير كافٍ",
details: stockCheck.data.items,
});
console.log(`الطلب ${order.id} مرفوض - مخزون غير كافٍ`);
}
} catch (error) {
console.error(`فشل معالجة الطلب ${orderData.orderId}:`, error);
order.status = "failed";
order.reason = "خطأ في المعالجة";
order.updatedAt = new Date().toISOString();
}
});
}
async function start() {
await rabbit.connect(process.env.RABBITMQ_URL);
await processOrderQueue();
app.listen(PORT, () => {
console.log(`خدمة الطلبات تعمل على المنفذ ${PORT}`);
});
}
start().catch(console.error);في الإنتاج، ستستخدم قاعدة بيانات حقيقية (PostgreSQL أو MongoDB) بدلاً من Maps في الذاكرة. النهج القائم على الذاكرة مستخدم هنا للتركيز على أنماط بنية الخدمات المصغرة دون عبء إعداد قاعدة البيانات.
الخطوة 6: بوابة API
بوابة API هي نقطة الدخول الوحيدة لجميع طلبات العميل. توجه حركة المرور، تتحقق من رموز JWT، وتطبق تحديد معدل الطلبات.
تثبيت التبعيات
cd api-gateway
npm install express http-proxy-middleware cors express-rate-limit jsonwebtoken
cd ..تنفيذ بوابة API
// 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";
// عناوين الخدمات
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",
};
// تحديد معدل الطلبات
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 دقيقة
max: 100,
message: { error: "طلبات كثيرة جداً، يرجى المحاولة لاحقاً" },
});
app.use(limiter);
// وسيط مصادقة JWT
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "المصادقة مطلوبة" });
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: "رمز غير صالح أو منتهي الصلاحية" });
}
}
// وسيط تسجيل الطلبات
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();
});
// فحص صحة جميع الخدمات
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(),
});
});
// المسارات العامة (لا تحتاج مصادقة)
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" },
})
);
// المسارات المحمية (تحتاج مصادقة)
app.use(
"/api/orders",
authenticate,
createProxyMiddleware({
target: SERVICES.orders,
changeOrigin: true,
pathRewrite: { "^/api/orders": "/orders" },
onProxyReq(proxyReq, req) {
// تمرير معلومات المستخدم للخدمة التالية
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" },
})
);
// معالج 404
app.use((req, res) => {
res.status(404).json({ error: "المسار غير موجود" });
});
// معالج الأخطاء
app.use((err, req, res, next) => {
console.error("خطأ في البوابة:", err.message);
res.status(500).json({ error: "خطأ داخلي في البوابة" });
});
app.listen(PORT, () => {
console.log(`بوابة API تعمل على المنفذ ${PORT}`);
console.log("مسارات الخدمات:");
Object.entries(SERVICES).forEach(([name, url]) => {
console.log(` /api/${name} -> ${url}`);
});
});الخطوة 7: تعبئة كل شيء في حاويات
Dockerfile للخدمات
أنشئ نفس Dockerfile لكل خدمة (api-gateway, user-service, product-service, order-service):
# api-gateway/Dockerfile (نفس النمط لجميع الخدمات)
FROM node:20-alpine
WORKDIR /app
# نسخ الوحدة المشتركة
COPY shared/ ./shared/
# نسخ ملفات الخدمة
COPY api-gateway/package*.json ./
RUN npm install --production
COPY api-gateway/src/ ./src/
EXPOSE 3000
CMD ["node", "src/index.js"]عدّل المسارات لكل خدمة:
# 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"]إعداد Docker Compose
# docker-compose.yml
version: "3.8"
services:
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672"
- "15672:15672" # واجهة الإدارة
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: مفتاح-jwt-سري-جداً-غيره-في-الإنتاج
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: مفتاح-jwt-سري-جداً-غيره-في-الإنتاج
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:الخطوة 8: التشغيل والاختبار
تشغيل جميع الخدمات
# بناء وتشغيل كل شيء
docker compose up --build -d
# التحقق من أن جميع الخدمات تعمل
docker compose ps
# عرض السجلات
docker compose logs -fيجب أن ترى خمس حاويات قيد التشغيل (RabbitMQ + 4 خدمات).
اختبار بوابة API
افتح طرفية جديدة واختبر التدفق الكامل:
# 1. فحص صحة البوابة
curl http://localhost:3000/health | jq
# 2. تسجيل مستخدم
curl -X POST http://localhost:3000/api/users/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "كلمةمرور123",
"name": "مستخدم تجريبي"
}' | jqاحفظ الرمز من الاستجابة، ثم تابع:
# 3. تصفح المنتجات (عام - لا يحتاج مصادقة)
curl http://localhost:3000/api/products | jq
# 4. إنشاء طلب (يحتاج مصادقة)
TOKEN="رمز-jwt-الخاص-بك"
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"userId": "معرف-المستخدم",
"items": [
{
"productId": "معرف-المنتج-من-الخطوة-3",
"quantity": 2,
"price": 49.99
}
]
}' | jq
# 5. التحقق من حالة الطلب (يجب أن يتغير من "pending" إلى "confirmed")
curl http://localhost:3000/api/orders/معرف_الطلب \
-H "Authorization: Bearer $TOKEN" | jqمراقبة RabbitMQ
افتح لوحة إدارة RabbitMQ على العنوان http://localhost:15672 (بيانات الدخول: admin / rabbitmq123). سترى:
- طوابير الانتظار:
order_created،order_confirmed،order_rejected،stock_updated - معدلات الرسائل: الرسائل المتدفقة بين الخدمات
- الاتصالات: اتصال واحد لكل خدمة
الخطوة 9: إضافة مرونة الخدمات
نمط قاطع الدائرة (Circuit Breaker)
أضف قاطع دائرة بسيطاً لحماية الخدمات من الأعطال المتتالية:
// 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("قاطع الدائرة مفتوح - الخدمة غير متاحة");
}
}
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("قاطع الدائرة مفتوح - أعطال كثيرة جداً");
}
}
getState() {
return {
state: this.state,
failures: this.failures,
threshold: this.failureThreshold,
};
}
}
module.exports = { CircuitBreaker };استخدمه في خدمة الطلبات عند استدعاء خدمة المنتجات:
const { CircuitBreaker } = require("../../shared/src/circuit-breaker");
const productServiceBreaker = new CircuitBreaker({
failureThreshold: 3,
resetTimeout: 15000,
});
// في دالة معالجة الطلبات:
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(`فشل فحص المخزون: ${response.status}`);
return response.json();
});الخطوة 10: إضافة تسجيل منظم
التسجيل المتسق عبر الخدمات يسهل كثيراً عملية التصحيح:
// 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 };الاستخدام في أي خدمة:
const { createLogger } = require("../../shared/src/logger");
const logger = createLogger("order-service");
logger.info("تم إنشاء الطلب", { orderId: order.id, userId: order.userId });
logger.error("فشل معالجة الطلب", { orderId, error: error.message });استكشاف الأخطاء وإصلاحها
رفض اتصال RabbitMQ
إذا فشلت الخدمات في الاتصال بـ RabbitMQ:
# التحقق من أن RabbitMQ يعمل
docker compose logs rabbitmq
# إعادة تشغيل RabbitMQ
docker compose restart rabbitmqعميل RabbitMQ المشترك يتضمن منطق إعادة المحاولة (10 محاولات مع تأخير 3 ثوانٍ)، مما يعالج ترتيب بدء التشغيل.
خدمة لا تستطيع الوصول لخدمة أخرى
داخل Docker Compose، تتواصل الخدمات عبر اسم الخدمة (مثلاً http://product-service:3002)، وليس localhost. تحقق من أن متغيرات البيئة مضبوطة بشكل صحيح.
تعارض المنافذ
إذا كانت المنافذ 3000-3003 أو 5672/15672 مستخدمة:
# العثور على ما يستخدم المنفذ
lsof -i :3000
# أو تغيير المنافذ في docker-compose.yml
ports:
- "4000:3000" # تعيين لمنفذ مضيف مختلفالخطوات التالية
الآن بعد أن لديك بنية خدمات مصغرة عاملة، إليك طرق لتوسيعها:
- إضافة قاعدة بيانات حقيقية: استبدال Maps في الذاكرة بـ PostgreSQL باستخدام Prisma أو Drizzle ORM
- إضافة خدمة إشعارات: استهلاك أحداث
order_confirmedلإرسال رسائل بريد إلكتروني عبر Resend - تنفيذ نمط الساغا: للمعاملات متعددة الخطوات التي تحتاج تراجعاً
- إضافة OpenTelemetry: التتبع الموزع عبر الخدمات
- النشر في الإنتاج: استخدام Kubernetes أو Docker Swarm للتنسيق
- إضافة بوابة GraphQL: استبدال وكيل REST بطبقة GraphQL موحدة
الخاتمة
لقد بنيت بنية خدمات مصغرة كاملة تتضمن:
- تقسيم الخدمات — كل نطاق (المستخدمون، المنتجات، الطلبات) له خدمته الخاصة
- المراسلة غير المتزامنة — RabbitMQ يفصل معالجة الطلبات عن التحقق من المخزون
- بوابة API — نقطة دخول واحدة مع المصادقة وتحديد المعدل وتوجيه الطلبات
- قاطع الدائرة — مرونة ضد الأعطال المتتالية
- التعبئة في حاويات — Docker Compose لعمليات نشر قابلة للتكرار
- التسجيل المنظم — سجلات JSON متسقة عبر جميع الخدمات
هذه البنية تتوسع أفقياً: يمكنك تشغيل عدة نسخ من أي خدمة خلف موازن أحمال، و RabbitMQ يوزع الرسائل تلقائياً بين المستهلكين. الأنماط المعروضة هنا — تقسيم الخدمات، المراسلة غير المتزامنة، بوابة API، قاطع الدائرة — هي نفس الأنماط المستخدمة في الإنتاج من قبل الشركات التي تعالج ملايين الطلبات يومياً.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

Docker Compose للمطورين: Next.js مع PostgreSQL و Redis
تعلم كيفية تغليف تطبيق Next.js كامل مع PostgreSQL و Redis باستخدام Docker Compose. يغطي هذا الدليل العملي تنسيق الخدمات المتعددة وسير عمل التطوير وإعادة التحميل الفوري وفحوصات الصحة والإعدادات الجاهزة للإنتاج.

بناء واجهات REST API جاهزة للإنتاج باستخدام FastAPI و PostgreSQL و Docker
تعلّم كيف تبني وتختبر وتنشر واجهة REST API احترافية باستخدام إطار FastAPI في بايثون مع PostgreSQL و SQLAlchemy و Alembic و Docker Compose — من الصفر حتى النشر.

بناء واجهات REST API باستخدام Go و Fiber: دليل عملي للمبتدئين
تعلّم كيف تبني واجهات REST API سريعة وجاهزة للإنتاج باستخدام لغة Go وإطار Fiber. يغطي هذا الدليل خطوة بخطوة إعداد المشروع، التوجيه، معالجة JSON، الربط بقاعدة البيانات عبر GORM، الوسطاء، معالجة الأخطاء، والاختبارات — من الصفر إلى واجهة API عاملة.