1. الساعة 3 الصبح وسيرفر الـ Production وقع
مش قصة خيالية.
مطور — خبرة 3 سنين، بيشتغل على مشروع بناه بنفسه من الصفر. السيرفر وقع، الـ clients بيتصلوا، المدير على الخط. فتح server.js…
4300 سطر.
دور على السطر اللي فيه المشكلة… ربع ساعة بيتمرر على الكود. لقى Bug صغير في الـ Payment Logic. عدّل سطرين. رفع. السيرفر رجع… قعد 5 دقاقي وبعدين وقع تاني. السطرين اللي عدلهم كسّروا حاجة تانية في الـ Order System — لأن الاتنين كانوا متشابكين في نفس الملف ومش واضح ليه.
ده مش إيه المطور غلط فيه. ده نتيجة قرار طبيعي جداً اتاخد في يوم الأول من المشروع: “هحط كل حاجة في ملف واحد والأول نشوف.”
القرار ده بيشتغل. لفترة. وبعدين بيانفجر.
الفيروس اللي بيكبر مع المشروع
اليوم التالت بنينا CRUD Server في server.js واحد — وكان منطقي. الـ Endpoints بسيطة، الكود قصير، كل حاجة واضحة. بس خد نفس الملف ده وضيف عليه 3 أشهر من التطوير في تيم من 3 ناس:
// server.js — بعد 3 أشهر من الـ "هنضيف بسرعة" 😬
const http = require('http');
const url = require('url');
const crypto = require('crypto');
const fs = require('fs');
const mailer = require('./mailer');
// =================== الداتا ===================
let users = [];
let products = [];
let orders = [];
let payments = [];
let reviews = [];
let sessions = [];
// =================== الـ Server ===================
const server = http.createServer(async (req, res) => {
const { pathname } = url.parse(req.url, true);
const method = req.method;
// ========= Users (120 سطر) =========
if (pathname === '/users' && method === 'GET') {
// Filtering + Pagination + Sorting... 40 سطر
} else if (pathname === '/users' && method === 'POST') {
// Validation + Business Rules + Email Notification... 80 سطر
} else if (pathname.match(/^\/users\/\d+$/) && method === 'PUT') {
// Validation + Update + Cache Invalidation... 60 سطر
// ========= Products (200 سطر) =========
} else if (pathname === '/products' && method === 'GET') { /* ... */ }
} else if (pathname === '/products' && method === 'POST') {
// نفس كود الـ Validation متكرر تاني! 🔁
if (!body.name) { /* ... */ }
if (!body.price || body.price < 0) { /* ... */ }
// ...
// ========= Orders (300 سطر) =========
} else if (pathname === '/orders' && method === 'POST') {
// تحقق من المخزون + احسب الأسعار + طبق الخصومات
// + Update الـ Inventory + سجل في الـ Audit Log
// + ابعت Confirmation Email (نفس كود الإيميل متكرر للمرة التالتة!)
// ... 300 سطر
// ========= Payments (200 سطر) =========
} else if (pathname === '/payments' && method === 'POST') {
// ... والمسيرة تكمل
}
// 40+ endpoint، 4000+ سطر
});
ده مش كود كتبه مطور تعبان. ده كود كتبه 4 ناس كويسين تحت ضغط الـ deadlines — كل واحد فيهم ضاف حاجة وفضل يشتغل.
والمشروع بقى وحش.
الأعراض الـ 5 اللي بتقولك “في مشكلة”
① خوف التعديل: كل ما تفتح الملف وعايز تعدل حاجة، في صوت جوّاك بيقول “انتبه — ممكن تكسر حاجة تانية.” ده مش حدس — ده تحذير حقيقي.
② الـ Copy-Paste كفيروس: كود الـ Email Notification موجود في 6 أماكن مختلفة. لما اكتشفت Bug فيه، لقيت إنك صلحته في 2 وفاتك 4. الـ Clients بلّغوا بمشكلة في الـ Emails لمدة أسبوع وأنت فاكر إنك خلصت.
③ Merge Conflicts دايمة: 3 مطورين بيعملوا Features مختلفة — كلهم بيعدلوا في server.js. كل PR = ساعتين Conflict Resolution.
④ الـ Onboarding المؤلم: مطور جديد انضم للتيم. طلبت منه يضيف Endpoint بسيطة. قعد 3 أيام بيحاول يفهم الكود قبل ما يكتب سطر.
⑤ Testing المستحيل: عايز تعمل Unit Test لـ Function واحدة؟ لازم تشغل السيرفر كامل مع كل الـ Database connections — لأن كل حاجة متداخلة في بعضها.
الحقيقة المُرّة: الكود ده مش “سيء”. هو بيشتغل. بس بيشتغل بشكل بيأكل وقتك ووقت فريقك كل يوم. والفاتورة بتتراكم.
2. المدينة، والقرية، والفرق بينهم
التشبيه اللي هيفضل معاك
تخيل قريتين:
قرية “المونو”: كل شيء في مبنى واحد كبير. المسجد والمدرسة والمستشفى والمحكمة والسوق — كلهم في نفس المكان. لما المبنى كان صغير، الموضوع كان بسيط. دلوقتي 10,000 نسمة والمبنى فيه 50 دور — ومحدش يعرف يلاقي حاجة، وأي إصلاح في الكهربا بيأثر على كل حاجة.
مدينة “سبرنشن”: كل حاجة في مكانها — الأحياء السكنية في ناحية، المستشفيات في ناحية، المدارس في ناحية، المحاكم في ناحية. لما محكمة احتاجت تترمم — بقية المدينة اشتغلت عادي. لما مدرسة جديدة اتبنت — ما أثرتش على المستشفيات.
الـ Codebase هو المدينة.
المشروع اللي فيه ملف واحد = قرية المونو. ممكن تشتغل. بس لما تكبر — بتنهار.
الـ Architecture الكويس = مدينة سبرنشن. كل حتة ليها مسؤولية واحدة واضحة. وأي تغيير في حتة مش بيأثر على الباقي.
ده اللي بنسميه: Separation of Concerns — فصل المسؤوليات.
3. كشف المشكلة: 6 مسؤوليات في 20 سطر
قبل ما نبني الحل، لازم نفهم المشكلة بدقة. هنرجع لـ POST Handler من اليوم التالت ونتريق عليه بعيون مختلفة:
// الكود ده من اليوم التالت — دلوقتي هنشوف الأمراض المخفية فيه ⬇️
if (pathname === '/users' && method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk.toString()); // 🔴 شغل Utils
req.on('end', () => {
try {
const data = JSON.parse(body); // 🔴 شغل Utils
} catch {
res.writeHead(400); res.end(); return;
}
// Validation
if (!data.name || !data.email) { // 🟡 شغل Controller
res.writeHead(400);
res.end(JSON.stringify({ error: 'بيانات ناقصة' }));
return;
}
// Business Rule: الإيميل فريد
if (users.find(u => u.email === data.email)) { // 🟢 شغل Service
res.writeHead(409);
res.end(JSON.stringify({ error: 'موجود قبل كده' }));
return;
}
// Data Access
const user = { id: nextId++, ...data }; // 🟢 شغل Service
users.push(user);
// Response
res.writeHead(201, { 'Content-Type': 'application/json' }); // 🔴 شغل Utils
res.end(JSON.stringify({ data: user })); // 🟡 شغل Controller
});
}
خمسة وعشرين سطر — ست مسؤوليات مختلفة في مكان واحد. كل واحدة منهم المفروض تكون في مكانها الصح.
4. الـ Architecture الجديد — أربع طبقات، أربع مسؤوليات
الهيكل أولاً — بعدين الكود
📁 my-api/
│
├── 📁 routes/ ← "فين المطعم أو المستشفى؟" التوجيه فقط
│ └── user.routes.js
│
├── 📁 controllers/ ← "اقرأ الطلب، نسّق، ابعت الرد"
│ └── user.controller.js
│
├── 📁 services/ ← "القلب النابض — Business Logic بدون HTTP"
│ └── user.service.js
│
├── 📁 utils/ ← "أدوات مشتركة — ملكوش حد معين"
│ ├── response.js
│ └── parseBody.js
│
└── server.js ← نقطة تجمع فقط — مش مكان Logic
Layer 1: الـ Routes — الريسبشن اللي مش بيعالج
الريسبشن في الفندق ملهوش دعوة بمحتوى أوضتك. وظيفته الوحيدة: يشوف اسمك في الكمبيوتر ويقولك “الأوضة 507 الدور الخامس.” انتهت مهمته.
// routes/user.routes.js
// وظيفة واحدة: "مين المسؤول عن الطلب ده؟"
// مفيش logic هنا — بس تشير
const userController = require('../controllers/user.controller');
const userRoutes = {
'GET:/users': userController.getAllUsers,
'GET:/users/:id': userController.getUserById,
'POST:/users': userController.createUser,
'PUT:/users/:id': userController.updateUser,
'DELETE:/users/:id': userController.deleteUser,
};
module.exports = userRoutes;
خمس أسطر. واضحة زي الشمس. أي حد يفتح الملف ده يعرف على الفور مين المسؤول عن كل Endpoint.
Layer 2: الـ Controller — المنسّق اللي مش متخصص
الـ Controller عنده 3 خطوات بالظبط، مش أكتر ولا أقل:
① اقرأ الـ Request (خد الـ Body والـ Params والـ Query)
② نادي الـ Service (ادي الداتا للمتخصص)
③ ابعت الـ Response (حوّل النتيجة لـ HTTP)
لو الـ Controller بيعمل حاجة تانية غير الـ 3 دول — في حاجة غلط في الـ Design.
// controllers/user.controller.js
const userService = require('../services/user.service');
const { parseBody } = require('../utils/parseBody');
const { sendResponse } = require('../utils/response');
const userController = {
// ① GET /users — جيب كل المستخدمين مع Pagination
async getAllUsers(req, res) {
const page = Math.max(parseInt(req.query?.page) || 1, 1);
const limit = Math.min(parseInt(req.query?.limit) || 10, 100);
const result = userService.getAll(page, limit); // ② نادي الـ Service
sendResponse(res, 200, result); // ③ ابعت الرد
},
// ① GET /users/:id — جيب مستخدم واحد
async getUserById(req, res) {
const user = userService.getById(req.params.id); // ② نادي الـ Service
if (!user) {
return sendResponse(res, 404, { error: 'المستخدم مش موجود' }); // ③
}
sendResponse(res, 200, { data: user }); // ③ ابعت الرد
},
// ① POST /users — أنشئ مستخدم جديد
async createUser(req, res) {
const body = await parseBody(req); // ① اقرأ الـ Body
if (!body?.name?.trim() || !body?.email?.trim()) { // ① Validate
return sendResponse(res, 400, { error: 'الاسم والإيميل مطلوبين' });
}
try {
const user = userService.create(body); // ② نادي الـ Service
sendResponse(res, 201, { data: user }); // ③ ابعت الرد
} catch (err) {
// الـ Service رمى Error — نترجمه لـ HTTP Status Code
if (err.message === 'EMAIL_EXISTS') {
return sendResponse(res, 409, { error: 'الإيميل موجود قبل كده' });
}
sendResponse(res, 500, { error: 'حصل خطأ' });
}
},
// ① PUT /users/:id — عدّل مستخدم
async updateUser(req, res) {
const body = await parseBody(req);
if (!body?.name?.trim() || !body?.email?.trim()) {
return sendResponse(res, 400, { error: 'الاسم والإيميل مطلوبين' });
}
try {
const updated = userService.update(req.params.id, body);
sendResponse(res, 200, { data: updated });
} catch (err) {
if (err.message === 'NOT_FOUND') return sendResponse(res, 404, { error: 'مش موجود' });
if (err.message === 'EMAIL_EXISTS') return sendResponse(res, 409, { error: 'الإيميل مأخود' });
sendResponse(res, 500, { error: 'حصل خطأ' });
}
},
// ① DELETE /users/:id — احذف مستخدم
async deleteUser(req, res) {
try {
const deleted = userService.remove(req.params.id);
sendResponse(res, 200, { message: `تم حذف "${deleted.name}"` });
} catch (err) {
if (err.message === 'NOT_FOUND') return sendResponse(res, 404, { error: 'مش موجود' });
sendResponse(res, 500, { error: 'حصل خطأ' });
}
},
};
module.exports = userController;
Layer 3: الـ Service — الخبير اللي مش عارف HTTP
ده أهم ملف في الـ Codebase. وعنده قاعدة واحدة صارمة:
لا يوجد
req. لا يوجدres. لا يوجدstatusCode. ولاwriteHead. ولاend.
الـ Service بياخد بيانات. يعمل شغله. يرجع بيانات. أو يرمي Error. خلاص. مش دوره يعرف إن المشروع ده HTTP API أصلاً.
// services/user.service.js
// ملفوظ: في الملف ده — ابحث عن req أو res... مش هتلاقيها.
// ده مش نسيان — ده قرار معماري مقصود.
let users = [
{ id: 1, name: 'أحمد محمد', email: 'ahmed@example.com', role: 'admin' },
{ id: 2, name: 'سارة علي', email: 'sara@example.com', role: 'user' },
{ id: 3, name: 'محمد خالد', email: 'khalid@example.com', role: 'user' },
];
let nextId = 4;
const userService = {
getAll(page, limit) {
const start = (page - 1) * limit;
const sliced = users.slice(start, start + limit);
return {
data: sliced,
total: users.length,
page,
totalPages: Math.ceil(users.length / limit),
};
},
getById(id) {
return users.find(u => u.id === id) ?? null;
},
create(data) {
// Business Rule: الإيميل فريد في النظام
if (users.find(u => u.email === data.email)) {
throw new Error('EMAIL_EXISTS');
}
const user = {
id: nextId++,
name: data.name.trim(),
email: data.email.toLowerCase().trim(),
role: data.role ?? 'user',
};
users.push(user);
return user;
},
update(id, data) {
const index = users.findIndex(u => u.id === id);
if (index === -1) throw new Error('NOT_FOUND');
// Business Rule: الإيميل الجديد مفيش حد تاني شايله
if (users.find(u => u.email === data.email && u.id !== id)) {
throw new Error('EMAIL_EXISTS');
}
users[index] = {
...users[index],
name: data.name.trim(),
email: data.email.toLowerCase().trim(),
};
return users[index];
},
remove(id) {
const index = users.findIndex(u => u.id === id);
if (index === -1) throw new Error('NOT_FOUND');
return users.splice(index, 1)[0];
},
};
module.exports = userService;
ليه ده مهم جداً؟ لأن نفس الـ Service ده — بدون أي تعديل — يشتغل مع:
// REST API (النهارده):
app.post('/users', async (req, res) => {
const user = await userService.create(req.body);
res.json(user);
});
// CLI Script (بكره):
const user = await userService.create({ name: 'Ahmed', email: 'a@b.com' });
console.log('Created:', user);
// GraphQL Resolver (بعد بكره):
createUser: (_, args) => userService.create(args),
// Scheduled Job (لو احتجنا):
await userService.create(importedRow);
كُتب مرة، اشتغل في 4 أماكن. ده اللي بيفرق.
Layer 4: الـ Utils — صندوق الأدوات
// utils/response.js — الشكل الموحد لكل Response في التطبيق
function sendResponse(res, statusCode, data) {
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
});
res.end(JSON.stringify(data));
}
module.exports = { sendResponse };
// utils/parseBody.js — قراءة الـ Body من الـ Stream بأمان
function parseBody(req) {
return new Promise((resolve, reject) => {
if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
return resolve(null);
}
let raw = '';
let size = 0;
const MAX = 1 * 1024 * 1024; // 1MB
req.on('data', chunk => {
size += chunk.length;
if (size > MAX) {
req.destroy();
return reject(new Error('PAYLOAD_TOO_LARGE'));
}
raw += chunk.toString();
});
req.on('end', () => {
if (!raw.trim()) return resolve(null);
try { resolve(JSON.parse(raw)); }
catch { reject(new Error('INVALID_JSON')); }
});
req.on('error', reject);
});
}
module.exports = { parseBody };
الـ Server.js بعد التحول — نظيف ومنظور
// server.js — دوره التنسيق بس، مش Logic
const http = require('http');
const { URL } = require('url');
const userRoutes = require('./routes/user.routes');
function matchRoute(method, pathname, routes) {
for (const [key, handler] of Object.entries(routes)) {
const [routeMethod, routePath] = key.split(':');
if (routeMethod !== method) continue;
const rParts = routePath.split('/').filter(Boolean);
const uParts = pathname.split('/').filter(Boolean);
if (rParts.length !== uParts.length) continue;
const params = {};
let ok = true;
for (let i = 0; i < rParts.length; i++) {
if (rParts[i][0] === ':') {
const raw = uParts[i];
params[rParts[i].slice(1)] = isNaN(raw) ? raw : +raw;
} else if (rParts[i] !== uParts[i]) {
ok = false; break;
}
}
if (ok) return { handler, params };
}
return null;
}
function handleRequest(req, res) {
const parsed = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(parsed.searchParams);
req.query = query;
req.params = {};
const match = matchRoute(req.method, parsed.pathname, userRoutes);
if (match) {
req.params = match.params;
match.handler(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Route not found' }));
}
}
const PORT = process.env.PORT || 3000;
http.createServer(handleRequest).listen(PORT, () =>
console.log(`✅ http://localhost:${PORT}`)
);
إيه اللي بيحصل في الكود ده؟ خطوة بخطوة
لو أول مرة تشوف حاجة زي كده، خلّينا نكسرها لقطع صغيرة. الهدف: أي واحد يقرا يمسك الفكرة ويبقى فاهم إيه اللي تحت الغطاء.
١ — الاستعداد: استيراد وإعداد الـ routes
require('http') → عشان نعمل سيرفر.
require('url') → عشان نفكّك الـ URL (pathname + query).
require('./routes/user.routes') → الـ routes شكلها: { "GET:/users": handler, "POST:/users": handler, "GET:/users/:id": handler }
يعني userRoutes عبارة عن object: المفتاح نص من نوع "METHOD:path" (مثلاً "GET:/users" أو "GET:/users/:id")، والقيمة هي الـ handler اللي هننادي عليه لو الطلب طابق المسار.
٢ — دالة matchRoute(method, pathname, routes)
دي بتقول: جالي طلب بـ method معيّن و pathname معيّن (مثلاً GET و /users/5). جوا الـ routes فيه مسار زي GET:/users/:id. هل الطلب ده يطابق؟ لو أيوه نرجع الـ handler + params (زي { id: 5 }). لو لأ نرجع null.
الـ key إزاي؟
الـ key بالشكل"GET:/users/:id". بنقسمه على أول:فيطلعrouteMethodوroutePath.
مثال:routeMethod = "GET"وroutePath = "/users/:id".لو الـ method مش زي طلب المستخدم نكمل للـ route اللي بعدها (
continue).تقسيم المسار لأجزاء:
routePathمثل"/users/:id"→ نقسم على/ونشيل الفاضي →["users", ":id"].
نفس الحاجة للـ URL اللي جالينا:pathnameزي/users/5→["users", "5"].لو عدد الأجزاء مش متساوي (مثلاً
/usersvs/users/5) مش هيطابق،continue.المقارنة جزء جزء:
- لو الجزء في الـ route بيبدأ بـ
:(مثلاً:id) ده متحول ديناميكي: بنحط قيمته من الـ URL فيparams(مثلاًparams.id = 5). لو القيمة رقمية بنحولها لـ number عشان نسهّل على الـ handler. - لو الجزء مش
:...بنقارنه حرفياً. لو مختلف →ok = falseونقطع.
- لو الجزء في الـ route بيبدأ بـ
لو كل الأجزاء اتطابقت نرجع
{ handler, params }. لو مفيش أي route طابق نرجعnull.
مثال سريع: طلب GET /users/7 مع routes فيها "GET:/users/:id":method يطابق، أجزاء المسار ["users","7"] و ["users",":id"] → نفس الطول، :id ياخد القيمة 7 → نرجع الـ handler مع params = { id: 7 }.
٣ — دالة handleRequest(req, res)
دي الـ callback اللي الـ HTTP Server بينادي عليها لكل طلب وارد.
فك الـ URL:
new URL(req.url, ...)بيعطينا كائن فيهpathnameوsearchParams. الـ query string بنحولها لـ object عادي:req.query(مثلاً?search=ahmed→req.query.search = "ahmed").تهيئة الـ params:
بنحطreq.params = {}من الأول؛ لو لقينا route يطابق هنملأها منmatch.params.المطابقة:
match = matchRoute(req.method, parsed.pathname, userRoutes).- لو
matchموجود: نملّيreq.paramsمنmatch.paramsوننادي الـ handler:match.handler(req, res). - لو
match === null: مفيش route لده الـ method + path → نرد 404 ورسالة JSON إن الـ route مش موجود.
- لو
٤ — تشغيل السيرفر
http.createServer(handleRequest)بيعمل سيرفر، وكل طلب بيوصل يتبعت لـhandleRequest..listen(PORT, ...)يربط السيرفر على البورت (منprocess.env.PORTأو 3000) والـ callback بتاعة الـconsole.logبتتنفذ لما السيرفر يفتح فعلاً.
الخلاصة في جملة:
الـ server.js ده ما بيعملش business logic. دوره: يستقبل الطلب، يفك الـ URL والـ query، يطابق الـ method والـ path مع الـ routes، ويسلّم الطلب للـ handler المناسب مع req.params و req.query. لو مفيش تطابق يرد 404. كده تكون فاهم إيه اللي بيحصل خطوة بخطوة وتقدر تبني عليه وتفضل تنين مجنح.
تدفق الـ Request عبر الطبقات

القانون الذي لا يُكسر: البيانات بتمشي في اتجاه واحد — من فوق لتحت. الـ Routes بتكلم الـ Controllers. الـ Controllers بتكلم الـ Services. الـ Services بتكلم الـ Data Layer. محدش بيكلم اللي فوقيه.
5. CommonJS و ESM — حرب النظامين
لو سألت نفسك يوماً: “ليه require ومش import؟”
JavaScript اتولدت سنة 1995 للمتصفح. وقتها مكانش فيه Modules أصلاً — كل الكود في <script> tags وكل المتغيرات Global.
<!-- 1999 — الجحيم العظيم -->
<script src="utils.js"></script> <!-- var name = "helper" -->
<script src="app.js"></script> <!-- var name = "app" → كتب فوق الأول! -->
<!-- لو ملفين عندهم نفس المتغير = آخر واحد يكسب ويكسّر الباقي -->
2009: Ryan Dahl عمل Node.js وكان محتاج Module System. الـ Browser ملوش حل، فاخترعوا CommonJS — نظام بيعزل كل ملف في Scope خاص.
// CommonJS — 2009: الحل اللي اخترعوه لـ Node.js
// utils.js
function formatDate(d) { return d.toISOString(); }
module.exports = { formatDate };
// app.js
const { formatDate } = require('./utils'); // بسيط، synchronous، واضح
2015: الـ TC39 (لجنة معايير JavaScript) أضافوا ESM للمواصفات الرسمية للغة نفسها — مش اختراع خارجي.
// ESM — 2015: المعيار الرسمي
export function formatDate(d) { return d.toISOString(); }
import { formatDate } from './utils.js'; // لازم الامتداد!
الفروق اللي بتأثر عليك فعلاً
① وقت التحميل — الفرق الجوهري
CommonJS بيتنفذ في Runtime — يعني الكود بيتنفذ سطراً سطراً، ولما يوصل لـ require() يوقف، يفتح الملف، يقرأه، يرجع الـ export، وبعدين يكمل. ده بيعني إنك تقدر تحط require() في أي مكان في الكود — حتى جوه if.
// CommonJS — بيتنفذ في Runtime:
const fs = require('fs'); // يوقف هنا، يقرأ مكتبة fs، يكمل
// ✅ تقدر تعمل conditional import:
if (process.env.NODE_ENV === 'development') {
const devLogger = require('./dev-logger'); // بيتحمل بس لو Development
devLogger.verbose('Debug mode on');
}
// في الـ Production? السطرين دول ما بيتنفذوش خالص — مش بيتحملوا
ESM بيتحل في Parse Time — يعني قبل ما أي كود يتنفذ أصلاً، الـ JavaScript Engine بيقرأ الملف كله وبيجمع كل الـ import statements ويحملها بالتوازي. لذلك مش ممكن تحط import جوه if — اللغة مش عارفة وقت الـ Parse إيه اللي if هيتحقق من ناحيته.
// ESM — import لازم في أعلى الملف، قبل أي كود:
import fs from 'node:fs';
import path from 'node:path';
// الاتنين دول بيتحملوا بالتوازي قبل أي سطر تاني يتنفذ
// ❌ ده مش هيشتغل:
if (process.env.NODE_ENV === 'development') {
import devLogger from './dev-logger'; // SyntaxError — مش مسموح هنا
}
// ✅ الحل: الـ Dynamic import — بيرجع Promise:
let devLogger;
if (process.env.NODE_ENV === 'development') {
const mod = await import('./dev-logger.js'); // بيشتغل! بس بيرجع Promise
devLogger = mod.default;
}
تخيّل الفرق كده: CommonJS = بتطبخ وبتجيب المكونات من التلاجة وانت بتطبخ واحدة واحدة. ESM = بتكتب قائمة المكونات قبل ما تبدأ، وكل حاجة بتتحضّر في نفس الوقت قبل ما النار تولع.
② الـ __dirname — المتغير اللي مش موجود في ESM
في CommonJS، Node بيحقن تلقائياً في كل ملف متغيرين مجانيين:
__filename= المسار الكامل للملف الحالي__dirname= المجلد اللي الملف ده موجود فيه
// CommonJS — جاهزين في أي ملف من غير ما تعمل import لأي حاجة:
console.log(__filename); // '/home/user/project/src/utils.js'
console.log(__dirname); // '/home/user/project/src'
// بتستخدمهم مع path.join:
const configPath = path.join(__dirname, '..', 'config', 'app.json');
// '/home/user/project/config/app.json' ← مسار صحيح بغض النظر من فين شغّلت الكود
في ESM، المتغيرين دول مش موجودين. Node مش بيحقنهم. بدلاً منهم، عندك import.meta.url اللي بيديك URL الملف الحالي (بالفورمات file:///...). ولازم تحوله بإيدك:
// ESM — لازم تعمله يدوياً في كل ملف محتاجه:
import { fileURLToPath } from 'node:url'; // بيحول file:/// URL لـ path عادي
import { dirname } from 'node:path'; // بياخد path ويرجع المجلد بتاعه
// import.meta.url = 'file:///home/user/project/src/utils.js'
const __filename = fileURLToPath(import.meta.url);
// __filename = '/home/user/project/src/utils.js'
const __dirname = dirname(__filename);
// __dirname = '/home/user/project/src'
// دلوقتي تقدر تستخدمهم زي CommonJS:
const configPath = path.join(__dirname, '..', 'config', 'app.json');
مش bug ولا نسيان. ESM مصمم يشتغل في المتصفح كمان، والمتصفح مش عنده “مجلدات” ولا ملفات. عشان كده الـ Meta Information اختلفت —
import.meta.urlبيعمل الـ concept نفس الفكرة بشكل portable.
③ Tree Shaking — ليه ESM أخف في الـ Production
Tree Shaking هو عملية بيعملها الـ Bundler (Webpack, Rollup, esbuild) إنه يشيل الكود اللي ما بيتستخدمش من الـ Bundle النهائي — عشان ملف الـ JavaScript اللي بيتنزل على المتصفح يكون أصغر وأسرع.
// math.js — مكتبة فيها 5 functions
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
export function power(a, b) { return Math.pow(a, b); }
// app.js — بتستخدم function واحدة بس
import { add } from './math.js';
console.log(add(2, 3));
مع ESM: الـ Bundler بيعرف وقت البناء (Build Time) — قبل ما الكود يشتغل — إنك استخدمت add بس. بيحط add في الـ Bundle ويحذف الـ 4 التانيين تلقائياً. حجم الـ Bundle: صغير.
مع CommonJS:
const math = require('./math'); // بيحمّل الملف كله
math.add(2, 3);
الـ Bundler مش عارف وقت البناء إيه اللي هتستخدمه من math — لأن require بيحصل في Runtime وممكن تكتب math[someVariable]() ويشتغل. فبيحط الملف كله في الـ Bundle “على كل حال”. حجم الـ Bundle: أكبر.
| الجانب | CommonJS | ESM |
|---|---|---|
| التحميل | Synchronous (Runtime) | Static (Parse Time) |
| Dynamic import | require() في أي مكان | await import() بيرجع Promise |
__dirname | تلقائي مجاناً | لازم تعمله يدوياً |
| Tree Shaking | ❌ مش ممكن | ✅ الـ Bundler يشيل الزيادة |
| Top-level await | ❌ مش موجود | ✅ ممكن تستخدم await في أعلى الملف |
| Default في Node | ✅ الـ default | محتاج "type":"module" في package.json |
الـ Circular Dependency — الوحش الصامت
الـ Circular Dependency بتحصل لما ملف A بيعمل require من ملف B، وملف B كمان بيعمل require من ملف A.
// a.js
const b = require('./b');
console.log('في a.js، قيمة b.name:', b.name);
module.exports = { name: 'Module A' };
// b.js
const a = require('./a');
console.log('في b.js، قيمة a.name:', a.name);
module.exports = { name: 'Module B' };
إيه اللي بيحصل خطوة بخطوة — بدون أي magic:
- Node يبدأ يحمّل
a.jsويدخله في سجل “الملفات اللي بتتحمل حالياً”. - أول سطر في
a.js:require('./b')→ يبدأ يحمّلb.js. - أول سطر في
b.js:require('./a')→ يدور علىa.jsفي السجل. - يلاقيه “بيتحمل حالياً” → عشان يتجنب اللوبة اللانهائية بيقول: “هبعتلك اللي اتصدّر من
a.jsلحد دلوقتي”. - اللي اتصدّر لحد دلوقتي =
{}(فاضي!) لأنmodule.exportsفيa.jsما وصلنا له لسه. b.jsبياخدa = {}ويكمل →a.name=undefined.b.jsيخلص، يرجع لـa.jsيكمل.
// الكود ده بيشتغل — من غير أي Error:
// في a.js، قيمة b.name: Module B ✅ (b اتحمل كاملاً)
// في b.js، قيمة a.name: undefined ❌ (a اتبعت فاضية!)
الخطورة: مش Error، مش Exception — بس قيم undefined في أماكن مش متوقعة. الـ Tests ممكن تعدي عادي وتلاقي البلوى بعدين في الـ Production بعد ساعات تحقيق.
الحل الصح: افصل الـ Shared Logic في ملف تالت مشترك، وكسر الدائرة:
// shared.js — ملف ثالت فيه اللي الاتنين محتاجينه
module.exports = { BASE_URL: 'http://api.example.com' };
// a.js — بياخد من shared، مفيش require('./b')
const { BASE_URL } = require('./shared');
module.exports = { name: 'Module A', url: BASE_URL };
// b.js — بياخد من shared، مفيش require('./a')
const { BASE_URL } = require('./shared');
module.exports = { name: 'Module B', url: BASE_URL };
// الدائرة اتكسرت ✅ — كل ملف بياخد من مصدر واحد مش بيحتاجه
إيه اللي تختاره؟
مشروع جديد من الصفر؟ → ESM + "type":"module" في package.json
مشروع قايم فيه require؟ → CommonJS — متغيرش حاجة من غير سبب
بتبني مكتبة للنشر (npm)? → ادعم الاتنين (Dual Package)
الأهم دايماً: الاتساق (Consistency) — متخلطش require مع import في نفس المشروع
6. الـ Dependency Injection — من “بجيبه بنفسي” لـ “خليه ييجيلي”
تشبيه ٣٠ ثانية: عايز قهوة
تخيل إنك في شغلانة (الـ Controller): مهمتك إنك تاخد الطلبات وترد بالنتيجة. محتاج قهوة (الـ Service) عشان تكمل.
- الطريقة الأولى — “بجيبه بنفسي”: كل ما تحتاج قهوة، تنزل من المكتب، تمشي لمحل القهوة اللي في شارع معين، تطلب، وترجع. أنت عارف العنوان بالظبط — ولو المحل اتغيّر أو اتقفل، أنت اللي هتعدّل مسارك. ولو حد عايز يختبر شغلانتك “بدون قهوة حقيقية”؟ صعب — أنت مرتبط بالمحل.
- الطريقة الثانية — “خليه ييجيلي”: حد (الـ برنامج أو الـ Composition Root) بيجيبلك القهوة على الطاولة. أنت مش بتدخل المطبخ. مش فارقك القهوة جاية من ستاربكس ولا من ماك — المهم إنها واصلة. ولو واحد عايز يختبرك، يقدر يجيبلك كوباية فاضية (Mock) وتكمل شغلانتك عادي.
الـ Dependency Injection = الطريقة الثانية. اللي محتاج حاجة (Controller) مش بيجيبها بنفسه — بياخدها جاهزة من بره (كـ Parameter). واللي بيجيب (مثلاً server.js) هو اللي يقرر: النهاردة قهوة عادية، بكره نسكافيه، أو حتى “قهوة وهمية” عشان التست.
المشكلة: الـ Controller “عارف أكتر من اللازم”
دلوقتي الـ Controller بتاعنا فيه السطر ده:
const userService = require('../services/user.service');
يعني إيه؟ إن الـ Controller قرر بنفسه إنه هيجيب الـ Service من ملف معين في مسار معين. ده اسمه Tight Coupling — ارتباط محكم. الـ Controller بقى عارف تفاصيل تنفيذية (وين الـ Service، وإزاي يجيبه) — وده بيخلق 3 مشاكل:
- مشكلة ① — الـ Testing بقى صعب: عشان تختبر الـ Controller، لازم تشغّل الـ
user.serviceالحقيقي — اللي ممكن يتصل بـ Database أو يبعت إيميلات. مش هتقدر “تقلّب القهوة بكوباية فاضية” — محكوم بالواقع. - مشكلة ② — التبديل بقى مؤلم: عايز تبدل من تخزين In-Memory لـ PostgreSQL؟ هتعدّل في كل Controller بيعمل
requireمنuser.service. أي تغيير في مصدر الداتا = تعديل في أماكن كتير. - مشكلة ③ — الـ A/B Testing مستحيل: عايز تجرب
Serviceجديدة على 10% من الطلبات بس؟ مفيش طريقة نظيفة — لأن الـ Controller هو اللي “ماسك الخيوط” ومش هيقبل نسختين.
الحل في جملة واحدة
متجيبش اللي محتاجه بنفسك — خلي حد يجيبهولك.
يعني: الـ Controller مش يعمل require('../services/user.service'). الـ Controller ياخد الـ Service كـ Parameter من بره — والملف اللي بيشغّل التطبيق (server.js أو ما يسمّى Composition Root) هو اللي يقرر: النهاردة نمرّر إيه. نفس الفكرة بتاعة “القهوة على الطاولة”.
الكود: قبل وبعد
// ❌ قبل (Tight Coupling): الـ Controller بيجيب الـ Service بنفسه
const userService = require('../services/user.service');
const userController = {
createUser(req, res) {
userService.create(req.body); // مرتبط بـ user.service تحديداً
}
};
// ✅ بعد (Dependency Injection): الـ Service بييجيله كـ Parameter
function createUserController(userService) { // أي Service تطبق نفس الـ Interface
return {
createUser(req, res) {
userService.create(req.body); // نفس الكود — بس مرن
},
getAllUsers(req, res) {
userService.getAll(/* ... */);
},
// باقي الـ Methods...
};
}
module.exports = createUserController;
الفرق الجوهري: الـ Controller بقى Function بتاخد userService من بره وترجع Object. مفيش require جوا الـ Controller — فمش عارف ولا مهتم منين الـ Service جاي. المهم إن اللي يتمرّر له فيه الـ Methods اللي محتاجها (create, getAll, …).
// server.js — نقطة الـ Wiring (Composition Root)
const createUserController = require('./controllers/user.controller');
const inMemoryService = require('./services/user.memory.service');
const postgresService = require('./services/user.postgres.service');
// النهاردة: In-Memory
const userController = createUserController(inMemoryService);
// بكره: PostgreSQL — من غير ما تلمس الـ Controller
// const userController = createUserController(postgresService);
كل الـ “مين يجيب إيه” بيحصل في مكان واحد — server.js. الـ Controller نظيف ومش مربوط بملف معين.
المكسب الكبير: التست بقى سهل
لما الـ Service بييجي من بره، تقدر في التست تمرّر Mock — نسخة وهمية فيها نفس الـ Interface بس من غير Database أو Network:
// tests/user.controller.test.js
const createUserController = require('../controllers/user.controller');
// Mock Service — بيحاكي السلوك بدون داتابيز أو Network
const mockService = {
create: jest.fn().mockReturnValue({ id: 99, name: 'Test User' }),
getAll: jest.fn().mockReturnValue({ data: [], total: 0, page: 1 }),
getById: jest.fn().mockReturnValue(null),
update: jest.fn(),
remove: jest.fn(),
};
const controller = createUserController(mockService);
test('createUser: بيرجع 201 لو الـ Body صحيح', async () => {
const req = {
method: 'POST',
query: {},
on: jest.fn((event, cb) => event === 'end' && cb()),
};
// ... إعداد الـ req و res وتشغيل الـ Assertion
expect(mockService.create).toHaveBeenCalled();
// ✅ اتختبر الـ Controller من غير سيرفر، من غير داتابيز
});
نتيجة: الـ Controller اتختبر من غير سيرفر، من غير داتابيز، ومن غير أي خدمة حقيقية — لأنك استبدلتها بـ “كوباية فاضية” (Mock) والسلوك ثابت.
لو لسه مش واخد بالك — ركّز على النقطة دي
الـ Dependency Injection مش Framework ومش مكتبة. ده أسلوب تفكير: أي جزء في الكود (مثل الـ Controller) مش بيقرر منين يجيب اللي محتاجه — بياخده جاهز من بره. اللي بيقرر هو مكان واحد (Composition Root) عشان التبديل والاختبار يبقوا سهل.
في Java وC# ممكن تلاقي DI Containers أوتوماتيكية — بس الفكرة واحدة: متجيبش اللي محتاجه بنفسك — خلي حد يجيبهولك. في Node تقدر تطبّقها بـ Functions عادية زي ما شفنا.
القاعدة الذهبية: لو الكلاس أو الـ Module بيقول require('./...') عشان يجيب خدمة بيستخدمها — اسأل: “هل ممكن أبدّل الخدمة دي من بره؟” لو لأ — ده Tight Coupling. حوّله لـ Parameter يتبعت من مكان واحد، وهتبقى طبقت DI وتبقى تنين مجنح.
7. التشبيه الكبير — المدينة بتنمو
في بداية البوست اتكلمنا عن القرية والمدينة. دلوقتي:
- الـ Layers = أحياء المدينة — كل حي ليه وظيفة محددة وحدوده الواضحة.
- الـ Separation of Concerns = قوانين تقسيم المناطق (Zoning Laws) — المصانع مش في المناطق السكنية.
- CommonJS = طرق قديمة — ESM = مترو أحدث — الاتنين بيوصلوك، بس المترو أسرع وأكفأ للمدن الكبيرة.
- الـ DI = شبكة المواصلات العامة — بدل ما كل مبنى يشتري عربيته الخاصة، السيارة (الـ Service) بتيجي للباب عند الطلب — وممكن تتبدل لأي وقت ما دامت بتوصل للنقطة المطلوبة.
8. ساحة الاختبار (The Crucible)
ده الجزء اللي بيقلّبك من “فاهم الموضوع” لـ “قادر تشرحه وتجاوب عليه تحت الضغط” — زي الانترفيو. كل سؤال تحت: أول حاجة ليه السؤال ده بييجي، بعدين الإجابة اللي تفتكرها، وفي الآخر قاعدة صغيرة عشان تبقى تنين مجنح.
أ) أسئلة الانترفيو
١. لغز الـ Circular Dependency
السؤال: order.service بيعمل require('./user.service') وuser.service بيعمل require('./order.service'). هيطلع Error ولا لأ؟
ليه بيسألوك كده؟ عشان يتأكد إنك فاهم إن Node مش بيفضّ الدائرة — بيعدّي منها، والنتيجة مش خطأ واضح بل قيم غلط. اللي ما عندهوش الخبرة بيقول “هيقع Error”؛ اللي فاهم بيقول “مش هيقع، وده أخطر.”
الإجابة:
مش هيقع Error — وده أخطر. Node بيرجع Partially Loaded Module (Object فاضي أو ناقص) بدل الـ Service الكامل. الكود يشتغل عادي لكن بقيم
undefinedفي أماكن غير متوقعة. الحل: افصل الـ Shared Logic في ملف ثالت (shared.service.js) — أو استخدم DI عشان تكسر الدائرة (محدش يجيب حد، الـ wiring يبقى من بره).
إيه يعني “محدش يجيب حد، الـ wiring يبقى من بره”؟
الدائرة بتحصل لأن كل ملف بيجيب التاني: order.service يعمل require('./user.service') وuser.service يعمل require('./order.service') → Node يدور في حلقة. مع الـ DI: مفيش ملف بيجيب التاني. كل Service يبقى Factory (Function) بتاخد اللي محتاجاه كـ Parameter. اللي بيجيب كل حاجة ويربطهم هو ملف واحد بره (مثلاً app.js أو server.js) — فمفيش require بين الـ Services فمفيش دائرة.
كود — قبل (دائرة):
// order.service.js — بيجيب user.service
const userService = require('./user.service');
module.exports = {
createOrder(data) {
const user = userService.getById(data.userId);
// ...
},
};
// user.service.js — بيجيب order.service → دائرة!
const orderService = require('./order.service');
module.exports = {
getById(id) { /* ... */ },
getOrdersForUser(userId) {
return orderService.getByUserId(userId);
},
};
كود — بعد (DI: مفيش حد بيجيب حد):
// order.service.js — مفيش require لـ user.service؛ بياخده من بره
function createOrderService(userService) {
return {
createOrder(data) {
const user = userService.getById(data.userId);
// ...
},
};
}
module.exports = createOrderService;
// user.service.js — مفيش require لـ order.service؛ بياخده من بره
function createUserService(orderService) {
let orders = orderService; // ممكن يتحدّث بعدين لو مرّرنا null
return {
setOrderService(os) { orders = os; },
getById(id) { /* ... */ },
getOrdersForUser(userId) {
return orders ? orders.getByUserId(userId) : [];
},
};
}
module.exports = createUserService;
// app.js — الـ Wiring كله هنا؛ هو الوحيد اللي يعمل require للـ Services
const createOrderService = require('./order.service');
const createUserService = require('./user.service');
// نعمل userService الأول (بدون orderService)، بعدين orderService، بعدين نربط
const userService = createUserService(null);
const orderService = createOrderService(userService);
userService.setOrderService(orderService); // دلوقتي الاتنين مربوطين — من بره، من غير دائرة
النتيجة: order.service.js وuser.service.js مابيعملوش require لبعض — فالـ load order مابيقفش في دائرة. الـ wiring كله في app.js.
القاعدة اللي تفتكرها: “دائرة = مفيش Error، بس قيم ناقصة. الحل: كسر الدائرة — ملف ثالت أو DI.”
٢. سؤال المسؤوليات
السؤال: بعد ما المستخدم يتسجل، عايزين نبعتله Welcome Email. الكود ده فين؟ Controller ولا Service؟
ليه بيسألوك كده؟ عشان يختبر إنك فاهم الفرق بين “مسؤولية الـ HTTP” و”قاعدة العمل (Business Rule)”. اللي يخلط بينهم بيحط كل حاجة في الـ Controller؛ اللي فاهم بيحط القواعد في الـ Service.
الإجابة:
في الـ Service (أو في
notification.serviceمخصوص بينادي عليه الـuser.service). مش في الـ Controller. السبب: بعت الإيميل ده Business Rule — مش جزء من بروتوكول الـ HTTP. لو بكره عملت CLI Tool تستورد Users، المفروض الـ CLI كمان يبعت Welcome Emails؛ لو الكود في الـ Controller، الـ CLI مش هيبعت. الـ Controller وظيفته: ترجمة الطلب والرد (HTTP ↔ استدعاء Services).
القاعدة اللي تفتكرها: “لو الحاجة منطق عمل (قاعدة مش HTTP) → Service. الـ Controller بس يترجم ويودّي.”
٣. سؤال الـ ESM الصعب
السؤال: كتبت import fs from 'fs' في ملف .js عادي. إيه اللي بيحصل؟
ليه بيسألوك كده؟ عشان كثيرين بيحاولوا ESM من غير ما يعدّلوا إعداد Node — فيقعوا في الخطأ ده. الممتحن عايز يعرف إنك فاهم إن الـ .js افتراضياً CommonJS.
الإجابة:
SyntaxError: Cannot use import statement outside a module. Node بيعامل الملفات
.jsكـ CommonJS بشكل افتراضي. عشان يستخدمimportلازم إما تسمي الملف.mjsأو تضيف"type": "module"فيpackage.json.
القاعدة اللي تفتكرها: “.js = CommonJS بالافتراض. عايز ESM → .mjs أو type: module.”
ب) لو فتحت الانترفيو ونسيت التفاصيل
- Circular: مفيش Error، في قيم ناقصة — كسّر الدائرة (ملف ثالت أو DI).
- Controller vs Service: قواعد العمل في الـ Service؛ الـ Controller يترجم HTTP فقط.
- import في .js: خطأ إلا لو
.mjsأو"type":"module".
لو حفظت التلات قواعد دول، تقدر تشرح وتكمّل من فهمك
9. خلاصة اليوم — المدينة اللي بنيناها
من الآخر: الكود الكويس مش اللي بيشتغل — ده اللي يتعدل ويكبر وفريق كامل يفهمه من غير ما يخاف. الـ Layered Architecture بتفصل المسؤوليات: الـ Routes بتوجه، الـ Controllers بتنسّق، الـ Services بتنفذ الـ Business Logic، والـ Utils بتخدم الكل. والـ DI مش Framework — ده أسلوب تفكير: بدل ما الكود يجيب ما يحتاجه، خلي حد يجيهوله.
في اليوم الجاي هندخل في عالم الـ Async والـ Error Handling — إزاي تتحكم في العمليات الغير متزامنة وتبني منظومة Errors بتحمي السيرفر بدلاً من إنها تقعه.
