MongoDB: دمج Express مع Mongoose: بنية تطبيقات الويب

آخر تحديث: 2026-08-26

تُعد «Express + Mongoose» مجموعة أدوات تطوير الويب الأكثر نضجًا في منظومة Node.js — وإتقانها يمكّنك من إنشاء واجهات برمجة تطبيقات (APIs) جاهزة للاستخدام في بيئة الإنتاج.

مكانة Express بين أطر عمل الويب في Node.js: تنقسم أطر عمل الويب في Node.js إلى ثلاث مستويات — 1. المستوى المنخفض (وحدة http): الخيار الأقل حجمًا، لكنه يتطلب التعامل يدويًّا مع التوجيه والتحليل ومعالجة الأخطاء، مما يجعله غير عملي؛ 2. المستوى المتوسط (Express/Koa): يوفر إمكانيات أساسية مثل التوجيه والبرمجيات الوسيطة ومعالجة الأخطاء؛ وهو مرن ولكنه يتطلب تجميعًا مخصصًا (يجب اختيار ORM والتحقق من الصحة والتسجيل، وما إلى ذلك)؛ 3. المستوى الأعلى (NestJS/AdonisJS): حلول كاملة مدمجة (DI/ORM/التحقق من الصحة/التسجيل)، جاهزة للاستخدام فورًا ولكن مع منحنى تعلم حاد. الأسباب التي تجعل Express يحتل الحصة السوقية الأكبر — 1. نواة بسيطة (تتعامل فقط مع التوجيه والبرمجيات الوسيطة؛ يمكن اختيار المكونات الأخرى بحرية)؛ 2. أغنى نظام بيئي للبرمجيات الوسيطة (يفوق عدد حزم البرمجيات الوسيطة الخاصة بـ Express على npm عددها في الأطر الأخرى بكثير)؛ 3. منحنى تعلم منخفض (إتقان واجهات برمجة التطبيقات الأساسية في ساعتين). التوصية: المشاريع الشخصية/الفرق الصغيرة → Express (مرنة وسريعة)؛ المشاريع على مستوى المؤسسات → NestJS (موحدة وشاملة).

مزايا وقيود الجمع بين Express و Mongoose: تتمثل مزايا هذا الجمع في: 1. أكبر مجتمع: ستجد أكبر عدد من الإجابات عند البحث عن حلول؛ حيث تغطي الأسئلة المتعلقة بـ Express و Mongoose على موقع Stack Overflow أوسع نطاق من الموضوعات؛ 2. المرونة: لا يرتبط Express بأي ORM، لذا يمكنك استبدال Mongoose بـ Prisma أو TypeORM في أي وقت؛ 3. التدرج: بدءًا من أبسط app.get، يمكنك إضافة البرمجيات الوسيطة والطبقات والتحقق من الصحة تدريجيًا، مما يؤدي إلى منحنى تعلم سلس. القيود — 1. عدم وجود أمان الأنواع (تفتقر مشاريع JavaScript إلى فحص أنواع البيانات في وقت التحويل البرمجي؛ وتتطلب استخدام TypeScript وتعريفات الواجهات)؛ 2. عدم وجود قواعد مدمجة (تعتمد بنية المشروع والتسمية والطبقات كليًّا على قواعد الفريق؛ وقد ينتهي الأمر بالمبتدئين بسهولة إلى كتابة كود معقد وغير منظم)؛ 3. أسلوب قائم على الاستدعاءات المرتدة (يعتمد Express على الاستدعاءات المرتدة؛ وتتطلب الأخطاء غير المتزامنة التغليف أو استخدام express-async-errors). يساعد فهم هذه القيود في وضع المواصفات الفنية في مرحلة مبكرة من المشروع لتجنبها.

1. ما ستتعلمه



2. أساسيات Express 4.x

ما هو Express؟ Express هو إطار عمل تطبيقات الويب الأكثر شيوعًا لـ Node.js، حيث يوفر نظام توجيه بسيطًا، وآلية برمجيات وسيطة، وأساليب مساعدة لـ HTTP. وهو ليس «إطار عمل متكامل»، بل هو طبقة بسيطة للتوجيه والبرمجيات الوسيطة — وهذه هي بالضبط نقطة قوته: فهو مرن وقابل للتركيب، ويحظى بدعم من نظام بيئي غني.

المبادئ الأساسية لتصميم Express: يعتمد Express على نموذج «مجموعة البرامج الوسيطة» — حيث يمر كل طلب HTTP عبر سلسلة من وظائف البرامج الوسيطة، يمكن لكل منها قراءة الطلب (req)، أو تعديل الاستجابة (res)، أو تمرير التحكم إلى وظيفة البرنامج الوسيط التالية (next). يسمح هذا النموذج، الذي يشبه البصل، بدمج الميزات كوحدات بناء: فالتسجيل، والمصادقة، والتحقق من الصحة، والمنطق التجاري، ومعالجة الأخطاء، لكل منها مكانه الخاص.

فلسفة البرمجيات الوسيطة في Express: تتمثل فلسفة التصميم الأساسية لـ Express في «البساطة + قابلية التجميع» — حيث يوفر الإطار نفسه مجرد تجريد لـ HTTP وآلية للبرمجيات الوسيطة، في حين يتم تنفيذ جميع الوظائف (التوجيه، والمصادقة، والتحقق من الصحة، والتسجيل) من خلال البرمجيات الوسيطة. على عكس النهج «الشامل» الذي تتبعه Django و Rails، لا يتضمن Express نموذج ORM مدمجًا، أو مصادقة، أو محرك قوالب — حيث يختار المطورون المكونات حسب الحاجة. المزايا: منحنى تعلم منخفض، ومرونة عالية، وقابلية استبدال قوية. العيوب: يتطلب بناء مجموعة التقنيات الخاصة بك، ويفتقر إلى المعايير الموحدة، وقد يختار المبتدئون المكونات الخاطئة بسهولة. يُعد Express مناسبًا لـ«الفرق ذات الخبرة التي تبني حلولًا حسب الطلب»، بينما يُعد كل من Django وRails مناسبين لـ«الفرق الصغيرة التي تحتاج إلى التسليم بسرعة».

معايير اختيار مجموعة التقنيات: لماذا نختار Express + Mongoose؟ 1. يُعد نظام Express الأكثر نضجًا (حيث يضم عددًا أكبر بكثير من برامج الوسيطة مقارنةً بـ Koa أو Fastify)، مما يسهل العثور على حلول عند ظهور المشكلات؛ 2. يوفر Mongoose ميزات متقدمة مثل التحقق من صحة المخطط، وبرامج الوسيطة، وpopulate، مما يقلل بشكل كبير من الكود النمطي مقارنةً ببرنامج التشغيل الأصلي؛ 3. منحنى التعلم سلس — نموذج req/res بديهي وسهل الفهم؛ 4. المجتمع نشط (عدد التنزيلات الأسبوعية من npm: أكثر من 30 مليون لـ Express وأكثر من مليون لـ Mongoose). يُعد Koa أكثر أناقة (دعم أصلي لـ async/await)، و Fastify أسرع (مُحسّن الأداء)، و NestJS أكثر توحيدًا (على غرار Angular)، لكن المزايا الإجمالية لـ Express —النظام البيئي، والوثائق، ومجموعة المواهب— تجعله الخيار الأفضل لكل من التعليم والإنتاج.

100%
sequenceDiagram
    participant Client
    participant Express as Express Server
    participant MW1 as Logging Middleware
    participant MW2 as Authentication Middleware
    participant MW3 as Validation Middleware
    participant Ctrl as Controller
    participant DB as MongoDB

    Client->>Express: HTTP Request
    Express->>MW1: req → res → next()
    MW1->>MW2: next()
    MW2->>MW3: Certification Approved
    MW3->>Ctrl: Verification Passed
    Ctrl->>DB: mongooseSearch
    DB-->>Ctrl: Search Results
    Ctrl-->>Client: JSON Response

Express مقابل أطر عمل Node.js الأخرى:

Dimension Express Koa Fastify NestJS
المفاهيم الأساسية مجموعة برامج الوسيطة نموذج البصل المكونات الإضافية عالية الأداء المُزيِّنات + الحقن التلقائي
الأداء المعيار أبطأ قليلاً أسرع بمقدار 2–3 أضعاف أبطأ قليلاً
نضج النظام البيئي ★★★★★ ★★★ ★★★ ★★★★
منحنى التعلم منخفض منخفض متوسط مرتفع
TypeScript يتطلب تهيئة يتطلب تهيئة دعم أصلي دعم أصلي
حالات الاستخدام واجهات برمجة التطبيقات (API) للأغراض العامة الخدمات الخفيفة واجهات برمجة التطبيقات (API) عالية التزامن تطبيقات المؤسسات

لماذا يُعد Express + Mongoose أفضل مزيج؟ يتوافق نموذج req/res في Express بشكل طبيعي مع استعلامات Mongoose غير المتزامنة — حيث تقبل دوال وحدة التحكم (Controller) المعلمة req، وتُجري استعلامًا على Mongoose، ثم تُرجع res.json()، مما ينتج عنه كود بديهي وسهل الصيانة.

برامج الوسيطة الرئيسية في منظومة Express: يعني التصميم البسيط لـ Express أن النواة تتولى التوجيه فقط، بينما يتم توفير الميزات الأخرى بواسطة برامج الوسيطة — 1. الأمان: helmet (تعيين رؤوس HTTP آمنة)، cors (مشاركة الموارد عبر الأصول)، express-rate-limit (تحديد معدل الطلبات)؛ 2. التحليل: express.json() (تحليل نصوص طلبات JSON)، express.urlencoded() (تحليل بيانات النماذج)، multer (تحميل الملفات)؛ 3. التسجيل: morgan (سجلات طلبات HTTP)، winston (سجلات التطبيق)؛ 4. الضغط: compression (يضغط نصوص الاستجابات باستخدام gzip، مما يقلل من استخدام النطاق الترددي بنسبة 60٪ أو أكثر)؛ 5. فحوصات الحالة: express-healthcheck (نقطة النهاية /health، وكشف موزع الحمل). يجب أن تقوم التطبيقات في بيئة الإنتاج بتثبيت كل من helmet و cors و morgan و compression على الأقل، حيث توفر هذه المكونات ضمانات أساسية للأمان والأداء.

(1) تثبيت المكونات التبعية

BASH
npm install express mongoose dotenv

(2) التطبيقات الأساسية

JAVASCRIPT
// === Basic Express Applications ===
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to ShopHub API' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});


3. تكوين اتصال Mongoose

كيفية عمل تجميع الاتصالات في Mongoose: بشكل افتراضي، يستخدم Mongoose تجميع الاتصالات الخاص ببرنامج تشغيل MongoDB لـ Node.js. ويحتفظ تجميع الاتصالات بمجموعة من اتصالات TCP القائمة؛ حيث تعيد الطلبات الجديدة استخدام الاتصالات الخاملة بدلاً من إنشاء اتصالات جديدة في كل مرة — مما يقلل من العبء الناتج عن عملية المصافحة الثلاثية لـ TCP ومصادقة MongoDB. ويحدد حجم تجميع الاتصالات عدد عمليات قاعدة البيانات التي يمكن للتطبيق تنفيذها في وقت واحد.

دورة حياة تجمع الاتصالات: يمر كل اتصال في تجمع الاتصالات بدورة الحياة التالية: «الإنشاء → الخمول → النشاط → الخمول → الإغلاق»—1. عند بدء تشغيل التطبيق، يقوم بإنشاء عدد من الاتصالات يساوي minPoolSize (لتحضير النظام وتجنب التأخير في الطلب الأول)؛ 2. عند وصول طلب، يتم استعارة اتصال خامل من المجموعة (الحالة النشطة)؛ 3. بعد اكتمال الطلب، يُعاد الاتصال إلى المجموعة (الحالة الخاملة)؛ 4. يُغلق الاتصال بعد انتهاء مهلة الخمول (maxIdleTimeMS) (لتوفير الموارد)؛ 5. عندما ينفد مخزون الاتصالات، تُوضع الطلبات الجديدة في قائمة الانتظار (يحدث خطأ بعد انتهاء مهلة waitQueueTimeoutMS). يساعد فهم دورة الحياة في تشخيص تسربات الاتصالات — فإذا استمر عدد الاتصالات النشطة في الزيادة دون إعادتها، فهذا يشير إلى أن أحد الطلبات قد فشل في تحرير الاتصال بشكل صحيح (عادةً بسبب نسيان استخدام await أو استثناء لم يتم التقاطه).

التعارض بين مجموعات الاتصالات ونموذج «بدون خادم»: تفترض مجموعات الاتصالات التقليدية أن التطبيقات تعمل لفترات طويلة وأنه يمكن إعادة استخدام الاتصالات. أما في بيئات «بدون خادم» (مثل AWS Lambda)، فإن كل عملية استدعاء تعمل كعملية جديدة — ولا يمكن إعادة استخدام مجموعات الاتصالات عبر عمليات الاستدعاء المختلفة، ويتم إنشاء اتصال جديد مع كل «بدء بارد». الحلول: 1. استخدام MongoDB Atlas Serverless (الذي يدير الاتصالات تلقائيًا)؛ 2. تهيئة mongoose خارج معالج Lambda (لإعادة استخدام اتصال مثيل الحاوية)؛ 3. استخدام برمجيات وسيطة لإدارة الاتصالات (مثل ذاكرة التخزين المؤقتة للاتصالات في mongoose)؛ 4. تقليل maxPoolSize (تتميز البيئات الخالية من الخوادم بتزامن منخفض، لذا فإن تجمع الاتصالات الكبير يهدر الموارد). تعد إدارة الاتصالات في البيئات الخالية من الخوادم + MongoDB إحدى نقاط الضعف التشغيلية.

كيف تعمل مجموعات الاتصالات:

100%
graph LR
    App1[Request 1] -->|Borrow| Pool[(Connection Pool<br/>min=5 max=50)]
    App2[Request 2] -->|Borrow| Pool
    App3[Request 3] -->|Waiting| Queue[Waiting Queue<br/>waitQueueTimeout]
    Pool -->|Connect 1| DB1[mongod]
    Pool -->|Connect 2| DB2[mongod]
    Pool -->|Connect N| DB3[mongod]
    App1 -.->|Return| Pool
    App3 -.->|Get the link| Pool

    style Pool fill:#d4edda
    style Queue fill:#fff3cd

شرح المعلمات الرئيسية للاتصال:

المعلمة القيمة الافتراضية الوصف توصيات التشغيل
maxPoolSize 100 الحد الأقصى لعدد الاتصالات 50 (يُعدَّل وفقًا لمعدل التزامن)
minPoolSize 0 الحد الأدنى لعدد الاتصالات الخاملة 5 (اتصالات الإعداد)
serverSelectionTimeoutMS 30000 مهلة اختيار الخادم 5000 (الفشل السريع)
socketTimeoutMS 0 مهلة اتصال السوكت 45000 (لمنع الاتصالات «الزومبي»)
maxIdleTimeMS 0 مهلة انتظار الاتصال 30000 (إعادة استخدام الاتصال)

نصائح لضبط معلمات الاتصال: لا توجد قيم موحدة تناسب جميع الحالات لمعلمات الاتصال؛ بل يجب ضبطها وفقًا للسيناريو الفعلي — 1. maxPoolSize: الصيغة = (حوالي 1 ميغابايت من الذاكرة لكل اتصال) + (عدد الطلبات المتزامنة × متوسط وقت الاستعلام). بالنسبة لخادم ذي 4 نوى وذاكرة 8 جيجابايت، يُوصى بقيمة تتراوح بين 50 و100؛ 2. serverSelectionTimeoutMS: اضبطها على 5000 (5 ثوانٍ) بدلاً من القيمة الافتراضية البالغة 30 ثانية — فالانتظار لمدة 30 ثانية قبل الإبلاغ عن خطأ عند عدم توفر قاعدة البيانات يعد بطيئًا للغاية؛ ويضمن مهلة 5 ثوانٍ اكتشاف الفشل بسرعة وتشغيل تنبيه؛ 3. heartbeatFrequencyMS: اضبطه على 10000 (10 ثوانٍ) لاكتشاف تبديل العقدة الرئيسية بسرعة أكبر (القيمة الافتراضية البالغة 10 ثوانٍ معقولة بالفعل)؛ 4. retryWrites: true: إعادة محاولة عملية الكتابة تلقائيًا مرة واحدة (يستعيد النظام تلقائيًا بعد انقطاعات الشبكة القصيرة، دون أن يلاحظ المستخدمون ذلك)؛ 5. مقاييس المراقبة: استخدام تجمع الاتصالات (النشط/الأقصى)، طول قائمة الانتظار، متوسط وقت الاستعارة. إذا تجاوز الاستخدام 80% باستمرار، فقم بالتوسع الأفقي (زيادة maxPoolSize أو إضافة خوادم).

مراقبة أحداث اتصال Mongoose: يُصدر كائن اتصال Mongoose (mongoose.connection) أحداثًا متنوعة لأغراض المراقبة — 1. connected: تم إنشاء الاتصال (إدخال في السجل)؛ 2. error: خطأ في الاتصال (يُطلق تنبيهًا)؛ 3. disconnected: فقدان الاتصال (يُطلق تنبيهًا؛ قد يتطلب الأمر إعادة الاتصال)؛ 4. reconnected: نجاح إعادة الاتصال التلقائي (إدخال في السجل؛ قد يحتاج التطبيق إلى إعادة التحقق من الحالة)؛ 5. close: إغلاق الاتصال (يُطلق أثناء الإغلاق السلس). في بيئات الإنتاج، يجب مراقبة جميع الأحداث وتسجيلها — فانقطاع الاتصال يمثل مشكلة خطيرة قد تؤثر على جميع عمليات قاعدة البيانات. حل المراقبة: mongoose.connection.on('error', logger.error) + mongoose.connection.on('disconnected', alertOps). | autoIndex | true | الفهرسة التلقائية | معطلة في بيئة الإنتاج: false |

حالات الاستخدام: التطبيقات الصغيرة (< 1,000 مستخدم نشط يوميًا) maxPoolSize=10–20؛ التطبيقات المتوسطة الحجم (1,000–100,000 مستخدم نشط يوميًا) 30–50؛ التطبيقات الكبيرة (> 100,000 مستخدم نشط يوميًا) 50–100. توخى الحذر عند تعيين القيمة فوق 100، حيث يستهلك كل اتصال حوالي 1 ميغابايت من الذاكرة.

(1) وظائف الاتصال

اعتبارات تصميم وظائف الاتصال: وظيفة الاتصال المخصصة للإنتاج لا تقتصر على سطر واحد من التعليمات البرمجية —mongoose.connect()— بل يجب أن تتولى ما يلي: 1. قراءة متغيرات البيئة (استرجاع MONGODB_URI من .env؛ لا تقم بترميزها بشكل ثابت)؛ 2. تكوين خيارات الاتصال (poolSize، timeout، autoIndex)؛ 3. الاستماع لأحداث الاتصال (تسجيل أحداث connected وerror وdisconnected)؛ 4. الإغلاق السلس عند إنهاء العملية (معالجة إشارات SIGINT/SIGTERM، mongoose.disconnect()); 5. استراتيجية إعادة المحاولة في حالة فشل الاتصال (التأخير الأسي لإعادة الاتصال). عادةً ما توضع دالة الاتصال في وحدة منفصلة، db.js، وهي أول دالة يتم استدعاؤها عند بدء تشغيل التطبيق.

JAVASCRIPT
// db.js
const mongoose = require('mongoose');

async function connectDB() {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      serverSelectionTimeoutMS: 5000,
      maxPoolSize: 50,        // Maximum Number of Connections
      minPoolSize: 5,         // Minimum Number of Connections
      socketTimeoutMS: 45000,
      autoIndex: true         // Automatic Indexing(Production can be shut down)
    });
    console.log(`✅ MongoDB connected: ${conn.connection.host}`);
    return conn;
  } catch (err) {
    console.error('❌ Connection failed:', err.message);
    process.exit(1);
  }
}

module.exports = { connectDB };

100%
graph TB
    Client[Client] -->|HTTP Request| Router[Express Router]

    Router -->|JWT Verification| AuthMW[Authentication Middleware]
    AuthMW -->|joi Verification| ValidateMW[Validation Middleware]
    ValidateMW --> Controller[Controller<br/>Business Logic]

    Controller -->|CRUD| Model[mongoose Model]
    Model -->|Search/Write| DB[(MongoDB)]

    Controller -->|Error| ErrorMW[Error-handling middleware]
    ErrorMW -->|Standard Response| Client

    style Model fill:#d4edda
    style Controller fill:#cce5ff

4. نمط MVC

مبدأ طبقات MVC: تتمثل القيمة الأساسية لنموذج MVC في فصل الاهتمامات — حيث يركز النموذج (Model) حصريًّا على البيانات («ماذا»)، وتركز الواجهة (View) حصريًّا على العرض («كيف يبدو»)، بينما يركز المتحكم (Controller) حصريًّا على التنسيق («كيف يتم ذلك»). في مشاريع واجهة برمجة التطبيقات (API)، تُختزل طبقة العرض إلى تسلسل JSON (res.json)، لكن مبدأ الفصل يظل كما هو: لتعديل قاعدة البيانات، ما عليك سوى تغيير النموذج؛ ولتعديل التوجيه، ما عليك سوى تغيير المسار؛ ولتعديل منطق الأعمال، ما عليك سوى تغيير وحدة التحكم. أما الكود الذي يفتقر إلى هذا الفصل فيخلط جميع المنطق معًا، مما يتطلب منك البحث في المشروع بأكمله لمجرد تغيير حقل واحد.

اتجاه التبعية في نموذج MVC: في بنية MVC الصارمة، يكون اتجاه التبعية أحادي الاتجاه — المسار → وحدة التحكم → النموذج. يعتمد المسار على وحدة التحكم (لاستدعاء دالة المعالج)، وتعتمد وحدة التحكم على النموذج (لاسترداد البيانات)، لكن النموذج لا يعتمد على وحدة التحكم (فطبقة البيانات لا تعلم من يستخدمها). تسمح هذه التبعية أحادية الاتجاه باختبار كل طبقة على حدة: يتم اختبار النموذج باستخدام اختبارات الوحدة، ووحدة التحكم باستخدام اختبارات التكامل، والمسار باستخدام اختبارات HTTP.

متى يتم إدخال طبقة الخدمات: عندما تصبح وحدة التحكم «مثقلة» (تتجاوز 50 سطراً أو تحتوي على عمليات قاعدة بيانات متعددة)، يجب استخراج المنطق التجاري إلى طبقة الخدمات. خصائص الخدمة: 1. لا تعتمد على Express (لا تعرف req أو res)؛ 2. تقبل معلمات بيانات بحتة وتُرجع نتائج بيانات بحتة؛ 3. يمكن إعادة استخدامها من قبل عدة وحدات تحكم (على سبيل المثال، يتم استدعاء createOrder من قبل كل من واجهة برمجة تطبيقات الويب ولوحة الإدارة)؛ 4. يمكن اختبارها بشكل مستقل (عن طريق محاكاة النموذج؛ ولا تتطلب أي طلبات HTTP). العلامات التي تشير إلى ضرورة إدخال خدمة: وجود عبارات if/else متداخلة في وحدات التحكم، ومنطق المعاملات الذي يمتد عبر عدة نماذج، والمنطق الحسابي القابل لإعادة الاستخدام.

فلسفة تصميم «النموذج السميك» في طبقة النموذج: تدعو Mongoose إلى اتباع «النموذج السميك» — الذي يتمثل في تغليف منطق الأعمال داخل الطرق الثابتة للنموذج، وطرق المثيلات، والبرمجيات الوسيطة. بالمقارنة مع "النموذج النحيف" (حيث يحدد النموذج المخطط فقط وتوجد جميع المنطق في وحدة التحكم)، تتمثل مزايا النموذج السميك في: 1. التماسك المنطقي (تتم معالجة التحقق من صحة كلمة المرور داخل طريقة المستخدم، بدلاً من توزيعها عبر وحدات تحكم متعددة)؛ 2. إعادة استخدام الكود (يمكن مشاركة User.comparePassword() بين كل من طرق تسجيل الدخول وتغيير كلمة المرور)؛ 3. التغليف (لا تحتاج وحدات التحكم إلى معرفة كيفية التحقق من كلمات المرور؛ فهي تستدعي الطريقة ببساطة). قيود نهج «النموذج السميك»: المنطق الذي يمتد عبر نماذج متعددة لا ينتمي إلى أي نموذج واحد ويجب وضعه في طبقة الخدمة.

مبادئ تصميم طبقة وحدة التحكم: تعمل وحدة التحكم كمنسق لمعالجة الطلبات — فهي تسترد البيانات من req، وتستدعي النموذج/الخدمة، وتُرجع res. يجب أن تظل وحدة التحكم «بسيطة» — 1. يجب أن تقتصر على استخراج المعلمات وإجراء تحويلات الأنواع (req.body → معلمات الخدمة)؛ 2. يجب أن تقوم باستدعاء واحد فقط للنموذج/الخدمة (يتم نقل المنطق المعقد إلى الخدمة)؛ 3. معالجة الأخطاء بشكل موحد (تغليفها في try/catch وتمريرها إلى البرمجيات الوسيطة الخاصة بالأخطاء عبر next(err))؛ 4. توحيد تنسيق الاستجابة (res.json({success: true, data})). معايير وحدة التحكم «البسيطة»: يجب ألا يتجاوز طول كل دالة في وحدة التحكم 30 سطرًا وألا تحتوي على أي منطق أعمال (يجب أن تقتصر عبارات if/else على التحقق من صحة المعلمات ومعالجة الأخطاء).

ترتيب تنفيذ برامج الوسيطة في Express: يحدد الترتيب الذي يتم به تسجيل برامج الوسيطة ترتيب التنفيذ — app.use(auth) → app.use(validate) → app.use(router) → app.use(errorHandler). الأخطاء الشائعة: 1. وضع برنامج الوسيطة الخاص بمعالجة الأخطاء قبل برنامج التوجيه (مما يؤدي إلى إرجاع صفحة خطأ لجميع الطلبات)؛ 2. وضع الوسيطة الخاصة بالمصادقة بعد المسارات التي لا تتطلب مصادقة (مما يؤدي إلى اعتراض بعض المسارات بشكل غير صحيح)؛ 3. وضع الوسيطة الخاصة بتحليل JSON (express.json()) بعد الموجه (مما يؤدي إلى أن req.body تصبح دائمًا undefined). قواعد ترتيب البرامج الوسيطة: ضع البرامج الوسيطة العامة أولاً (json، cors، helmet)، والبرامج الوسيطة الخاصة بالمسارات داخل جهاز التوجيه (auth، validate).

Middleware Combination Patterns: Express middleware supports flexible combinations—1. Serial combination: authenticate → authorize → controller, where the output of the previous step serves as the input for the next (authenticate sets req.user, and authorize checks req.user.role); 2. Conditional chaining: Certain routes require additional middleware (e.g., POST /products requires validate + authenticate, while GET /products does not require authenticate), implemented via route-level middleware; 3. Error bubbling: Any middleware that calls next(err) skips all subsequent regular middleware and proceeds directly to the error-handling middleware—this means validation failures, authentication failures, and database errors are all handled uniformly within the error middleware. Understanding middleware composition patterns enables you to design flexible and maintainable request-handling workflows.

الحد الفاصل بين البرمجيات الوسيطة والخدمات: تتولى البرمجيات الوسيطة معالجة الشؤون المشتركة (المصادقة، والتسجيل، ومعالجة الأخطاء)، بينما تتولى الخدمات معالجة منطق الأعمال (إنشاء الطلبات، وحساب التقييم). علامات عدم وضوح الحدود: 1. استعلامات قاعدة البيانات في البرمجيات الوسيطة (على سبيل المثال، من المعقول أن تستعلم البرمجيات الوسيطة الخاصة بالمصادقة عن مستخدم ما في قاعدة البيانات، ولكن ليس من المعقول أن تستعلم البرمجيات الوسيطة الخاصة بالتحقق من صحة البيانات عن منتج ما)؛ 2. قراءة الخدمات لـ req أو res (يجب أن تقبل الخدمات معلمات البيانات الخالصة فقط)؛ 3. وحدات التحكم التي تتكون من 2–3 أسطر من التعليمات البرمجية فقط (مما يشير إلى أن البرمجيات الوسيطة تقوم بأكثر من اللازم). حدود واضحة: تتولى البرمجيات الوسيطة مهام «مستوى الطلب» (من يمكنه الوصول، وما إذا كان الطلب صالحًا)، بينما تتولى الخدمات مهام «مستوى الأعمال» (كيفية معالجة البيانات، وكيفية تطبيق القواعد).

100%
graph TB
    subgraph "Route Layer"
        R1[GET /api/products]
        R2[POST /api/products]
        R3[GET /api/products/:sku]
    end

    subgraph "Controller Layer"
        C1[listProducts]
        C2[createProduct]
        C3[getProduct]
    end

    subgraph "Model Layer"
        M1[Product.find]
        M2[Product.create]
        M3[Product.findOne]
    end

    subgraph "MongoDB"
        DB1[(products Gathering)]
    end

    R1 --> C1 --> M1 --> DB1
    R2 --> C2 --> M2 --> DB1
    R3 --> C3 --> M3 --> DB1

    style R1 fill:#cce5ff
    style C1 fill:#d4edda
    style M1 fill:#fff3cd

(1) هيكل دليل المشروع

استراتيجيات لتوسيع بنية الدلائل: تُعد البنية الأساسية لنموذج MVC (النماذج/وحدات التحكم/المسارات/البرمجيات الوسيطة) مناسبة للمشاريع الصغيرة والمتوسطة الحجم. ومع نمو المشروع، يصبح من الضروري توسيع هذه البنية — 1. إضافة دليل validators/ (لفصل المخطط عن وحدة التحكم بهدف إعادة الاستخدام)؛ 2. إضافة دليل services/ (استخراج المنطق التجاري من وحدة التحكم إلى الخدمات، بحيث تتولى وحدة التحكم فقط تحويل الطلبات إلى استجابات)؛ 3. إضافة دليل config/ (للإدارة المركزية لاتصالات قاعدة البيانات ومتغيرات البيئة)؛ 4. إضافة دليل utils/ (للوظائف المساعدة العامة مثل أدوات التسجيل وأدوات JWT). مبدأ التوسع: يجب أن تقوم كل طبقة بمهمة واحدة فقط — مبدأ المسؤولية الواحدة.

TEXT 📖 للعرض فقط
models/      # Data Model(mongoose Schema)
controllers/ # Business Logic
routes/      # Route Definitions
middlewares/ # Middleware
services/    # Service Layer(Optional)
utils/       # Utility Functions

(1) طبقة النموذج

مبادئ تصميم طبقة النموذج: تُعد طبقة النموذج المصدر الوحيد الموثوق للبيانات — حيث يتم تعريف جميع تعريفات البيانات وقواعد التحقق من الصحة وطرق الاستعلام في النموذج؛ ويجب ألا تحتوي وحدات التحكم والمسارات على أي منطق متعلق بالبيانات. المبادئ المحددة: 1. تتضمن تعريفات المخطط جميع قواعد التحقق من الصحة (لم تعد وحدات التحكم تقوم بإجراء عمليات تحقق مكررة)؛ 2. يتم تعريف الفهارس في المخطط بدلاً من إنشائها يدويًّا (يقوم Mongoose بإنشاء الفهارس تلقائيًّا)؛ 3. يتم تعريف الحقول الافتراضية وطرق المثيلات والطرق الثابتة في المخطط (يتم تغليف منطق الأعمال داخل النموذج)؛ 4. select: false يخفي الحقول الحساسة (على سبيل المثال، لا يتم إرجاع passwordHash افتراضيًّا).

إعادة استخدام المخططات والوراثة: في المشاريع الكبيرة، قد تشترك نماذج متعددة في نفس البنية الفرعية — على سبيل المثال، يحتوي كل من User وAdmin على حقل address. طرق إعادة الاستخدام: 1. تعريف مخطط فرعي (const AddressSchema = new Schema({...})، ثم الإشارة إلى AddressSchema في كل من UserSchema وAdminSchema)؛ 2. إضافة الحقول ديناميكيًا باستخدام Schema.add()؛ 3. الوراثة عبر المُميِّزات (الفئة الأساسية Schema + حقول امتداد الفئات الفرعية، مثل الفئة الأساسية Product + الفئتين الفرعيتين Book وProduct). تُعد المخططات الفرعية نمط إعادة الاستخدام الأكثر شيوعًا — حيث يمكن أن يكون للمخططات المستقلة قواعد التحقق من الصحة والبرمجيات الوسيطة الخاصة بها.

استراتيجيات إعلان الفهارس: يدعم Mongoose ثلاث طرق لإعلان الفهارس — 1. الفهارس على مستوى الحقل ({sku: {type: String, index: true, unique: true}}، وهي بسيطة وبديهية ومناسبة للفهارس أحادية الحقل)؛ 2. الفهارس المركبة على مستوى المخطط (schema.index({category: 1, price: -1})، وهي مناسبة للفهارس المركبة متعددة الحقول)؛ 3. الفهارس النصية (schema.index({title: 'text', content: 'text'})، وهي مخصصة للبحث عن النص الكامل). ملاحظة لبيئات الإنتاج: اضبط autoIndex على false (لتجنب إنشاء الفهارس عند كل عملية بدء تشغيل؛ يجب إنشاء الفهارس يدويًّا أو عبر نصوص الترحيل). اضبطه على true في بيئات التطوير لتسهيل المزامنة التلقائية.

JAVASCRIPT
// models/Product.js
const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({
  sku: { type: String, required: true, unique: true, index: true },
  title: { type: String, required: true },
  price: { type: mongoose.Schema.Types.Decimal128, required: true },
  category: { type: String, required: true, index: true },
  stock: { type: Number, default: 0 }
}, { timestamps: true });

module.exports = mongoose.model('Product', ProductSchema);

(2) طبقة وحدة التحكم

مبادئ تصميم طبقة وحدة التحكم: تعمل وحدة التحكم كمنسق لمعالجة الطلبات — فهي تستخرج المعلمات من req، وتستدعي النموذج (Model) لاستعلام البيانات، وتُرجع استجابة عبر res. يجب أن تكون وحدة التحكم «رقيقة» وليس «سميكة» — 1. لا تحتوي على منطق الأعمال (يوجد منطق الأعمال في أساليب النموذج أو طبقة الخدمة)؛ 2. لا تتعامل مباشرةً مع قاعدة البيانات (تعمل بشكل غير مباشر من خلال أساليب النموذج)؛ 3. لا تقوم بتحويل البيانات (تُرجع نتيجة lean() مباشرةً باستخدام res.json)؛ 4. يتم تمرير الأخطاء إلى البرمجيات الوسيطة الخاصة بمعالجة الأخطاء عبر next(err) (لا تستخدم try-catch داخل وحدة التحكم ولا تقم بتعيين res.status مباشرةً).

JAVASCRIPT
// controllers/productController.js
const Product = require('../models/Product');

exports.listProducts = async (req, res) => {
  const { page = 1, limit = 20, category } = req.query;
  const query = { isActive: true };
  if (category) query.category = category;

  const products = await Product.find(query)
    .select('sku title price thumbnail')
    .limit(limit * 1)
    .skip((page - 1) * limit)
    .lean();

  res.json({ data: products, page, limit });
};

exports.getProduct = async (req, res) => {
  const product = await Product.findOne({ sku: req.params.sku })
    .select('-__v')
    .lean();

  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }
  res.json(product);
};

exports.createProduct = async (req, res) => {
  const product = await Product.create(req.body);
  res.status(201).json(product);
};

(3) طبقة المسار

مبادئ تصميم طبقة التوجيه: تتولى طبقة التوجيه فقط عملية ربط عناوين URL بوحدات التحكم — ولا تحتوي على أي منطق برمجي. ومع ذلك، يمكن إدراج برامج وسيطة في عملية الربط — حيث يمثل السطر router.post('/', authenticate, authorize('admin'), validate(createSchema), ctrl.create) سلسلة معالجة الطلب الكاملة: المصادقة → التفويض → التحقق من الصحة → المنطق التجاري. مسؤوليات طبقة التوجيه: 1. تحديد أنماط عناوين URL (بأسلوب RESTful)؛ 2. ربط طرق HTTP بطرق وحدة التحكم؛ 3. إرفاق البرامج الوسيطة (المصادقة، التفويض، التحقق من الصحة)؛ 4. عدم احتوائها على أي منطق لمعالجة البيانات.

التنظيم المعياري للمسارات: يتطلب توجيه المسارات في المشاريع الكبيرة اتباع نهج معياري — 1. تقسيم الملفات حسب المورد (routes/products.js، routes/orders.js، routes/users.js)، بحيث يحدد كل ملف مسارات لنوع واحد فقط من الموارد؛ 2. إنشاء مسارات معيارية باستخدام Express Router (const router = express.Router())، وتركيبها باستخدام app.use('/api/products', productRouter)؛ 3. استخدام المسارات المتداخلة للتعبير عن التسلسلات الهرمية للموارد (يتم التعامل مع /api/posts/:postId/comments بواسطة مسار التعليقات)؛ 4. تنفيذ إصدارات المسارات (app.use('/api/v1', v1Router)، app.use('/api/v2', v2Router)). يجعل التوجيه المعياري التعاون بين أعضاء الفريق أكثر كفاءة — حيث يكون كل شخص مسؤولاً عن ملف مسار واحد، مما يضمن عدم وجود تعارضات.

استراتيجيات دمج البرامج الوسيطة: يمكن دمج البرامج الوسيطة في Express لتنفيذ سلاسل معقدة لمعالجة الطلبات — 1. البرامج الوسيطة العامة (مستوى app.use): express.json()، cors()، helmet()، morgan() (التسجيل)، rateLimit() (تحديد معدل الاستخدام)؛ 2. برامج الوسيطة لمجموعات الموجه (router.use): authenticate (لمجموعات الموجه التي تتطلب تسجيل الدخول)، authorize('admin') (لمجموعة الموجه الإدارية)؛ 3. برامج الوسيطة لمسار واحد (مستوى المعلمة router.get): validate(schema) (منطق التحقق من صحة نقطة نهاية محددة). قوة تكوين البرمجيات الوسيطة — استدعاء واحد لـ router.post('/', auth, validate, ctrl.create) ينفذ الحماية الثلاثية المتمثلة في «المصادقة + التحقق من الصحة + منطق الأعمال»، مما يلغي الحاجة إلى تكرار هذا المنطق في وحدة التحكم.

JAVASCRIPT
// routes/products.js
const express = require('express');
const router = express.Router();
const ctrl = require('../controllers/productController');

router.get('/', ctrl.listProducts);
router.get('/:sku', ctrl.getProduct);
router.post('/', ctrl.createProduct);

module.exports = router;


5. برامج الوسيطة لمعالجة الأخطاء

سلاسل البرامج الوسيطة وانتشار الأخطاء: في Express، تشكل البرامج الوسيطة سلسلة من الاستدعاءات بناءً على ترتيب تسجيلها — حيث تقوم next() بتمرير التحكم إلى البرنامج الوسيط التالي، بينما تتخطى next(err) جميع البرامج الوسيطة العادية وتنتقل مباشرةً إلى البرنامج الوسيط المخصص لمعالجة الأخطاء. تعمل هذه الآلية على مركزية معالجة الأخطاء، مما يلغي الحاجة إلى كتابة كتل try-catch في كل مسار. ومع ذلك، لاحظ أن الاستثناءات داخل دوال async لا تؤدي تلقائيًا إلى تشغيل next(err)؛ بل يجب تغليفها في كتلة try-catch أو معالجتها تلقائيًا بواسطة express-async-errors.

أمن المعلومات في استجابات الأخطاء: لا تكشف أبدًا عن رسائل الأخطاء الأولية للعملاء في بيئة الإنتاج — فقد يؤدي ذلك إلى تسريب سلاسل اتصال قاعدة البيانات أو مسارات الملفات أو تتبعات المكدس. قواعد تعيين البرامج الوسيطة لمعالجة الأخطاء: ValidationError → 400 (تُرجع تفاصيل الخطأ على مستوى الحقل)، CastError → 400 (تُرجع «تنسيق معرّف غير صالح»)، E11000 → 409 (تُرجع اسم حقل مكرر)، الأخطاء الأخرى → 500 (تُرجع «خطأ داخلي» فقط؛ ويتم تسجيل التفاصيل).

الحاجة إلى express-async-errors: لا تدعم برامج الوسيطة في Express 4.x الدوال غير المتزامنة — فإذا تم إلقاء استثناء داخل برنامج وسيط غير متزامن، فلن يلتقطه Express، وسيظل الطلب معلقًا حتى انتهاء مهلة الانتظار. الحلول: 1. استخدام try-catch يدويًا + next(err) (تكرار في الكود)؛ 2. حزمة express-async-errors (يُغلف سطر واحد من require تلقائيًا جميع دوال معالجات المسارات)؛ 3. يدعم Express 5.x برامج الوسيطة غير المتزامنة بشكل أصلي. يُوصى بشدة باستخدام الخيار 2 في بيئات الإنتاج؛ ما عليك سوى إضافة require('express-async-errors') في أعلى app.js — دون الحاجة إلى تعديل أي كود موجود.

سياسة التسجيل: لا ينبغي أن تقتصر برامج الوسيطة الخاصة بمعالجة الأخطاء على إرجاع استجابة فحسب، بل يجب أن تسجل الأحداث أيضًا — ومع ذلك، يجب أن يختلف مستوى التفاصيل في السجلات حسب البيئة: في بيئة التطوير، يجب إخراج تتبع المكدس الكامل (لتسهيل عملية تصحيح الأخطاء)؛ أما في بيئة الإنتاج، فيجب إخراج نوع الخطأ ومعرّف الطلب فقط (لمنع كتابة المعلومات الحساسة في ملفات السجلات). نوصي باستخدام Winston أو Pino بدلاً من console.error — فهما يدعمان مستويات التسجيل (خطأ/تحذير/معلومات/تصحيح الأخطاء)، وتناوب السجلات، والإخراج المنظم (بتنسيق JSON لتسهيل استرجاع البيانات بواسطة ELK).

JAVASCRIPT
// middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
  console.error(err);

  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'ValidationError',
      details: Object.fromEntries(
        Object.entries(err.errors).map(([k, v]) => [k, v.message])
      )
    });
  }

  if (err.code === 11000) {
    return res.status(409).json({ error: 'Duplicate key', field: err.keyValue });
  }

  if (err.name === 'CastError') {
    return res.status(400).json({ error: 'Invalid ID format' });
  }

  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
};

module.exports = errorHandler;


6. هيكل التطبيق الكامل

تحليل متعمق لإدارة تجمع الاتصالات: يُعد تجمع الاتصالات في Mongoose آلية أساسية في برنامج تشغيل MongoDB لـ Node.js — حيث يحدد maxPoolSize الحد الأقصى لعدد عمليات قاعدة البيانات المتزامنة، بينما يقوم minPoolSize بتحضير الاتصالات الخاملة مسبقًا لتجنب تأخيرات «البدء البارد». عند استنفاد تجمع الاتصالات، تُوضع الطلبات الجديدة في قائمة انتظار (يتم التحكم فيها بواسطة waitQueueTimeoutMS)، ويتم الإبلاغ عن خطأ بعد انتهاء مهلة الانتظار. ضبط بيئة الإنتاج: 1. maxPoolSize = التزامن في التطبيق × 1.5 (لإتاحة هامش أمان)؛ 2. minPoolSize = maxPoolSize × 0.1 (للتسخين المسبق)؛ 3. اضبط serverSelectionTimeoutMS على 5000 (للفشل السريع بدلاً من الانتظار لمدة 30 ثانية)؛ 4. اضبط autoIndex على false في بيئة الإنتاج (حيث يؤدي إنشاء الفهرس إلى حظر الاتصالات).

تشخيص استنفاد مخزون الاتصالات: عندما يُبلغ التطبيق عن خطأ «Mongoose: انتهت مهلة الاتصال»، فإن الأسباب المحتملة تشمل: 1. حجم مخزون الاتصالات صغير جدًّا (maxPoolSize < العدد الفعلي للاتصالات المتزامنة؛ قم بزيادة حجم المخزون)؛ 2. الاستعلامات البطيئة التي تشغل الاتصالات (إذا استمر تشغيل استعلام لمدة 10 ثوانٍ دون تحرير الاتصال، فاستخدم db.currentOp() لتحديد الاستعلام البطيء وإنهائه)؛ 3. تسربات الاتصالات (على سبيل المثال، استدعاءات find() دون وجود await في الكود، أو عدم إغلاق المؤشرات)؛ 4. الحمل الزائد على خادم MongoDB (تحقق من وحدة المعالجة المركزية (CPU) والذاكرة وعمليات الإدخال/الإخراج للقرص). خطوات التشخيص: 1. تحقق من mongoose.connection.readyState (0 = غير متصل، 1 = متصل، 2 = متصل، 3 = قيد قطع الاتصال)؛ 2. تحقق من عدد الاتصالات النشطة باستخدام db.serverStatus().connections؛ 3. استخدم أداة APM (New Relic/Datadog) لتتبع مدة استعلامات قاعدة البيانات.

التعارض بين مجموعات الاتصالات والبيئة الخالية من الخوادم: في بيئة بدون خادم (AWS Lambda/Cloud Functions)، تنشئ كل مثيل وظيفة تجمع اتصالات خاص بها — إذا أدى 100 طلب متزامن إلى تشغيل 100 وظيفة Lambda، وكانت لكل وظيفة Lambda 5 اتصالات، فسيغمر ما مجموعه 500 اتصال قاعدة بيانات MongoDB. الحلول: 1. تقليل maxPoolSize بشكل كبير (يوصى بـ 5–10 لسيناريوهات Lambda)؛ 2. استخدام وكيل تجميع الاتصالات في MongoDB Atlas؛ 3. تهيئة mongoose.connect خارج معالج Lambda (لإعادة استخدام تجمع الاتصالات وتقليل عدد الاتصالات أثناء عمليات التشغيل البارد)؛ 4. النظر في استخدام واجهة برمجة تطبيقات HTTP بدلاً من الاتصالات المباشرة (على سبيل المثال، Data API أو Realm Web SDK).

مراقبة أحداث الاتصال: يصدر كائن الاتصال في Mongoose عدة أحداث يمكن استخدامها للمراقبة — 1. mongoose.connection.on('connected', ...): نجاح الاتصال بـ MongoDB (تسجيل في السجل)؛ 2. mongoose.connection.on('error', ...): خطأ في الاتصال (تنبيه + إعادة المحاولة)؛ 3. mongoose.connection.on('disconnected', ...): فقدان الاتصال (ربما بسبب مشكلة في الشبكة أو إعادة تشغيل MongoDB؛ ستحاول آلية إعادة الاتصال التلقائية استعادة الاتصال)؛ 4. mongoose.connection.on('reconnected', ...): تمت إعادة الاتصال بنجاح (تسجيل ذلك في السجل؛ قد تكون العمليات التجارية قد توقفت ويجب استعادتها)؛ 5. mongoose.connection.on('close', ...): تم إغلاق الاتصال (إيقاف تشغيل التطبيق أو mongoose.disconnect()). يجب مراقبة هذه الأحداث في بيئات الإنتاج؛ وإلا، فقد تؤدي حالات انقطاع الاتصال الصامتة إلى حوادث في بيئة الإنتاج حيث "تنتهي مهلة جميع استعلامات قاعدة البيانات".

أفضل الممارسات للإغلاق السلس: عند إغلاق التطبيق، يجب عليه إغلاق اتصال MongoDB بشكل سلس — 1. الاستماع إلى إشارات SIGINT/SIGTERM (Ctrl+C أو إشارات إنهاء بود Kubernetes)؛ 2. التوقف عن قبول الطلبات الجديدة (server.close())؛ 3. انتظار اكتمال الطلبات الجارية (الاستعلامات النشطة في تجمع الاتصالات)؛ 4. قطع الاتصال بـ Mongoose (mongoose.disconnect()); 5. إنهاء العملية (process.exit(0)). عواقب الإغلاق غير السلس: تتوقف عمليات قاعدة البيانات الجارية، مما قد يؤدي إلى عدم اتساق البيانات (مثل المستندات التي تمت كتابتها جزئيًا فقط). يمنح Kubernetes البودات 30 ثانية لإجراء إيقاف تشغيل سليم؛ ويجب على التطبيق إكمال الخطوات المذكورة أعلاه في غضون 30 ثانية.

100%
sequenceDiagram
    participant Main as app.js
    participant Dotenv as dotenv
    participant DB as connectDB
    participant Express as Express App
    participant Listen as app.listen

    Main->>Dotenv: Loading .env
    Dotenv-->>Main: Environment Variables Are Ready
    Main->>Express: Create app + Middleware
    Main->>Express: Registration Routes
    Main->>Express: Handling Registration Errors
    Main->>DB: await connectDB()
    DB-->>Main: ✅ MongoDB connected
    Main->>Listen: app.listen(PORT)
    Listen-->>Main: 🚀 Server running

ترتيب تسجيل البرامج الوسيطة: تقوم Express بتنفيذ البرامج الوسيطة حسب ترتيب تسجيلها، وهذا الترتيب بالغ الأهمية: JSON parsing → logging → routes → 404 handling → error handling. يجب وضع معالجة الأخطاء في النهاية.

تفاصيل تنفيذ الوسيطة 404: الوسيطة 404 هي وسيطة عادية لا تحتوي على معلمات مسار — يتم وضعها بعد جميع المسارات وتُنفَّذ عندما لا تتطابق أي من المسارات السابقة. التنفيذ: app.use((req, res) => res.status(404).json({error: 'Not Found'})). ملاحظة: برنامج الوسيط 404 ليس برنامج وسيط للخطأ (الذي يأخذ 4 معلمات)، بل هو برنامج وسيط عادي (الذي يأخذ 3 معلمات). ولا يتطلب next() نظرًا لعدم وجود وظائف برامج وسيطة لاحقة. الأخطاء الشائعة: وضع الوسيطة 404 قبل المسارات (مما يؤدي إلى إرجاع جميع الطلبات برمز 404)، أو نسيان تضمين الوسيطة 404 (يتم التعامل مع المسارات غير المطابقة بواسطة Express افتراضيًا، حيث يتم إرجاع رسالة بتنسيق HTML تقول “Cannot GET /xxx” بدلاً من JSON).

الترتيب البرمجيات الوسيطة الوظيفة
1 express.json() تحليل نص الطلب
2 cors() دعم عبر النطاقات
3 برامج الوسيطة الخاصة بالتسجيل سجلات الطلبات
4 التوجيه معالجة الأعمال
5 404 البرامج الوسيطة لا يوجد مسار مطابق
6 معالجة الأخطاء استجابات موحدة للأخطاء

إدارة تكوين البيئات: تحتاج مشاريع Express + Mongoose إلى إدارة التكوينات لبيئات متعددة (التطوير/الاختبار/الإنتاج) — 1. يخزن ملف .env تكوينات التطوير المحلية (عنوان URL لقاعدة البيانات، والمنفذ، والمعلومات السرية) ولا يتم إضافته إلى Git؛ 2. يتم إضافة ملف .env.example إلى Git كقالب؛ 3. يتم إدخال إعدادات الإنتاج عبر متغيرات البيئة (Docker/K8s/منصات السحابة) ولا تستخدم ملف .env؛ 4. يقوم الدليل config/ بتحميل إعدادات مختلفة بناءً على البيئة (config/development.js، config/production.js). المبدأ الأساسي: لا تقم أبدًا بكتابة المفاتيح أو كلمات المرور بشكل ثابت في الكود، ولا تقم أبدًا بإدراجها في Git.

الإغلاق السلس: يجب أن تدعم تطبيقات Express قيد التشغيل الإغلاق السلس — عند تلقي إشارة SIGTERM أو SIGINT: 1. التوقف عن قبول الطلبات الجديدة (server.close()); 2. الانتظار حتى تكتمل الطلبات المعلقة (تحديد مهلة، مثل 10 ثوانٍ)؛ 3. إغلاق اتصال Mongoose (mongoose.disconnect()); 4. إنهاء العملية. عواقب الفشل في تنفيذ الإغلاق السلس: يتم مقاطعة عمليات قاعدة البيانات الجارية قسريًّا، مما قد يؤدي إلى عدم اتساق البيانات. تعتمد التحديثات المتدرجة في Kubernetes/Docker على عمليات الإغلاق السلس — يجب أن تكمل البودات القديمة جميع الطلبات قبل انتهاء المهلة.

نقطة نهاية فحص الحالة: يجب أن توفر بيئة الإنتاج نقطة نهاية /health — التي تستخدمها أجهزة موازنة الحمل لتحديد ما إذا كان التطبيق في حالة جيدة، وكذلك تستخدمها اختبارات فحص الحالة/الاستعداد في Kubernetes. يجب أن يتضمن فحص الحالة ما يلي: 1. استجابة HTTP 200 (عملية التطبيق نشطة)؛ 2. mongoose.connection.readyState === 1 (اتصال قاعدة البيانات طبيعي)؛ 3. فحوصات اختيارية: اتصال Redis، ومساحة القرص، واستخدام الذاكرة. تنفيذ بسيط: app.get('/health', (req, res) => { if (mongoose.connection.readyState === 1) res.json({status: 'ok'}); else res.status(503).json({status: 'db disconnected'}); }). تكوين Kubernetes: يقوم livenessProbe بالفحص كل 10 ثوانٍ؛ وإذا فشل ثلاث مرات متتالية، يتم إعادة تشغيل البود. يقوم readinessProbe بالفحص كل 5 ثوانٍ؛ وإذا فشل مرتين متتاليتين، يتم إزالة البود من الخدمة.

نظام الدفاع الأمني متعدد الطبقات: لا يعتمد الأمان لتطبيقات Express على إجراء واحد، بل على نهج دفاعي متعدد الطبقات — 1. البرمجيات الوسيطة Helmet (تقوم بتعيين 12 رأسًا أمنيًا لـ HTTP: X-Content-Type-Options، X-Frame-Options، CSP، إلخ)؛ 2. سياسة CORS (تقوم الدالة cors() بإدراج المصادر المسموح بها في القائمة البيضاء؛ لا تقم بتعيين Access-Control-Allow-Origin: *)؛ 3. تحديد معدل الاستخدام (express-rate-limit لمنع هجمات القوة الغاشمة وهجمات DDoS)؛ 4. التحقق من صحة المدخلات (joi/express-validator لمنع هجمات الحقن)؛ 5. المصادقة والتفويض (JWT + RBAC لمنع العمليات غير المصرح بها)؛ 6. HTTPS (نقل البيانات المشفر بواسطة TLS لمنع هجمات الوسيط). توفر كل طبقة حماية مستقلة؛ ولا يؤدي اختراق أي طبقة بمفردها إلى المساس بفعالية الطبقات الأخرى.

نقاط نهاية فحص الحالة: يجب أن توفر تطبيقات الإنتاج نقطتي النهاية /health و/ready — حيث تتحقق /health من أن العملية قيد التشغيل (برمز 200 OK بسيط)، بينما تتحقق /ready من أنها جاهزة لقبول الطلبات (بما في ذلك ما إذا كان اتصال MongoDB يعمل أم لا). يستخدم Kubernetes هاتين النقطتين النهائيتين لإجراء اختبارات الفعالية واختبارات الاستعداد. تنفيذ /ready: try { await mongoose.connection.db.admin().ping() } catch { return res.status(503).json({ready: false}) }. إذا تعذر الوصول إلى MongoDB، فإن /ready تُرجع رمز الحالة 503، ويقوم Kubernetes بتعليق حركة المرور إلى ذلك البود (Pod) ولكنه لا يعيد تشغيله (على عكس اختبار الفعالية).

تصميم متقدم لفحوصات الحالة: فحوصات الحالة على مستوى الإنتاج لا تقتصر على مجرد إرسال اختبار اتصال إلى قاعدة البيانات — 1. فحوصات التبعية: التحقق من MongoDB (اختبار اتصال)، وRedis (اختبار اتصال)، وواجهات برمجة التطبيقات الخارجية (طلب HEAD) بالتسلسل؛ إذا تعذر الوصول إلى أي منها، يتم وضع علامة «غير جاهز» على النظام؛ 2. تأخير بدء التشغيل: لا تُصنف النظام على أنه «جاهز» فور بدء تشغيل التطبيق (فقد لا يزال اتصال Mongoose قيد الإنشاء)؛ انتظر حتى يتم تشغيل connection.on('connected') قبل تصنيفه على أنه جاهز؛ 3. التحكم في مهلة الانتظار: يجب ألا يتعطل فحص الحالة نفسه (اضبط مهلة انتظار مدتها 5 ثوانٍ؛ إذا انقضت المهلة، فصنفه على أنه «غير جاهز»)؛ 4. الوضع المتدهور: إذا تعذر الوصول إلى MongoDB لكن Redis متاح، فقم بإرجاع حالة «متدهور» (توفير البيانات المخزنة مؤقتًا بدلاً من عدم التوفر التام). تعمل فحوصات الحالة كجسر بين العمليات والتطوير — حيث يوفر المطورون نقاط النهاية، وتقوم فرق العمليات بتكوين أدوات الفحص، ويقوم فريق SRE بإعداد التنبيهات.

JAVASCRIPT
// app.js
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./db');
const productRoutes = require('./routes/products');
const errorHandler = require('./middlewares/errorHandler');

const app = express();

app.use(express.json());

app.get('/healthz', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

app.use('/api/products', productRoutes);

app.use(errorHandler);

(async () => {
  await connectDB();
  app.listen(process.env.PORT || 3000);
})();


7. متغيرات البيئة في dotenv

لماذا نحتاج إلى متغيرات البيئة؟ تنتشر سلاسل اتصال قواعد البيانات المبرمجة بشكل ثابت، ومفاتيح JWT، وأرقام المنافذ في جميع أنحاء الكود — مما يعني: ① أن أي تسرب للكود يؤدي إلى تسرب للمفاتيح؛ ② يتطلب تغيير البيئات (التطوير/الاختبار/الإنتاج) تعديل الكود؛ ③ عند التعاون، يكون لكل شخص تكوين مختلف. يتمثل حل dotenv في تحميل المتغيرات من ملف .env إلى process.env؛ حيث يقرأ الكود متغيرات البيئة فقط، وتستخدم البيئات المختلفة ملفات .env مختلفة.

تصنيف متغيرات البيئة من الناحية الأمنية: تُصنف متغيرات البيئة إلى ثلاثة مستويات بناءً على درجة حساسيتها — 1. التكوينات العامة (PORT، NODE_ENV، LOG_LEVEL): يمكن إدراجها في Git؛ غير حساسة؛ 2. التكوينات الداخلية (MONGODB_URI، REDIS_URL، API_BASE_URL): لا يتم تسجيلها في Git؛ تُشارك داخل الفريق؛ تُدرج عبر ملف .env أو CI/CD؛ 3. بيانات الاعتماد السرية (JWT_SECRET، AWS_ACCESS_KEY، DATABASE_PASSWORD): أعلى درجة من الحساسية؛ لا يتم تسجيلها في Git أو تخزينها في ملف .env؛ تُدرج عبر خدمة إدارة الأسرار (AWS Secrets Manager/HashiCorp Vault). يقلل نهج الإدارة المتدرج هذا من مخاطر «تخزين جميع التكوينات في ملف .env، مما يؤدي إلى اختراق كامل في حالة تسرب ملف .env».

أفضل الممارسات لإدارة المتغيرات البيئية:

التمرين الوصف
لا تقم بإدراج ملف .env في المستودع أضفه إلى ملف .gitignore لمنع الكشف عنه
حفظ ملف .env.example يوفر نموذجًا؛ ولا يحتوي على قيم فعلية
إدارة مفاتيح الإنتاج AWS Secrets Manager / HashiCorp Vault
التحقق من المتغيرات المطلوبة التحقق عند بدء التشغيل من وجود MONGODB_URI والمتغيرات الأخرى
فصل البيئات NODE_ENV=development/production

التحقق من متغيرات البيئة عند بدء التشغيل: عند بدء تشغيل التطبيق، يجب عليه التحقق من وجود جميع متغيرات البيئة المطلوبة — فإذا لم يتم تعيين MONGODB_URI، فستفشل جميع عمليات قاعدة البيانات بعد بدء تشغيل التطبيق، لذا من الأفضل إظهار رسالة خطأ والإنهاء عند بدء التشغيل. طرق التحقق: 1. التحقق البسيط (if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is required'))؛ 2. استخدام dotenv-safe (يقارن تلقائيًا بين .env و.env.example؛ ويُصدر خطأً في حالة فقدان أحد المتغيرات)؛ 3. استخدام joi للتحقق (تحديد configSchema للتأكد من صحة القيم الموجودة في process.env؛ على سبيل المثال، يجب أن يكون PORT رقمًا). في بيئة الإنتاج، نوصي باتباع النهج 3 — فهو لا يتحقق من وجود المتغيرات فحسب، بل يتحقق أيضًا من صحة القيم (على سبيل المثال، يجب أن يكون PORT رقمًا يتراوح بين 1024 و65535، ويجب أن يكون NODE_ENV أحد development أو test أو production).

التسلسل الهرمي للإعدادات: متغيرات البيئة > ملف .env > القيم الافتراضية. يجب كتابة الكود على النحو التالي: process.env.PORT || 3000—تُعطى الأولوية لمتغيرات البيئة؛ وإذا لم يتم تعيين أي منها، تُستخدم القيم الافتراضية.

BASH
# .env
MONGODB_URI=mongodb://localhost:27017/shopdb
PORT=3000
NODE_ENV=development
JWT_SECRET=your-secret-key
JAVASCRIPT
// config.js
require('dotenv').config();

module.exports = {
  port: parseInt(process.env.PORT) || 3000,
  mongoUri: process.env.MONGODB_URI,
  jwtSecret: process.env.JWT_SECRET,
  nodeEnv: process.env.NODE_ENV || 'development'
};


8. تدريب عملي: واجهة برمجة تطبيقات (API) كاملة لعمليات CRUD

مبادئ تصميم التوجيه في REST: استخدم الأسماء بصيغة الجمع لتسمية الموارد (مثل «products» بدلاً من «product»); استخدم المسارات الهرمية للموارد المتداخلة (مثل «/products/:id/reviews»); استخدم طرق HTTP للتعبير عن دلالات العمليات (GET للقراءة، وPOST للكتابة، وDELETE للحذف). استخدم البادئة «/auth» لمسارات المصادقة والبادئة «/api» لمسارات الأعمال.

الخلافات الشائعة في تصميم التوجيه: هناك عدة خلافات شائعة في تصميم التوجيه وفقًا لـ RESTful — 1. صيغة الجمع مقابل صيغة المفرد: /products أم /product؟ تفضل الأعراف السائدة في المجال صيغة الجمع (للإشارة إلى مجموعة)، لكن واجهة برمجة تطبيقات GitHub تستخدم صيغة المفرد. الاتساق أهم من أيهما تختار؛ 2. عمق التداخل: /products/:id/reviews/:reviewId أم /reviews/:reviewId؟ يُوصى بحد أقصى مستويين من التداخل؛ لأكثر من مستويين، استخدم موردًا من المستوى الأعلى بالإضافة إلى معلمات التصفية (على سبيل المثال، /reviews?productId=xxx)؛ 3. نقاط نهاية الإجراءات: هل /users/:id/activate متوافقة مع REST؟ بالمعنى الدقيق للكلمة، لا، لكنها شائعة في المشاريع الواقعية (فهي أكثر بديهية من PUT /users/:id {status: 'active'}). REST هي إرشادات، وليست عقيدة؛ أعط الأولوية لسهولة القراءة والعملية.

النقاط الرئيسية المتعلقة بأمن واجهة برمجة التطبيقات (API):

التدابير الأمنية التنفيذ
المصادقة رمز JWT من نوع «Bearer»
التفويض التحقق من دور RBAC (المسؤول/العميل)
التحقق من صحة المدخلات التحقق من صحة مخطط joi
تحديد معدل الاستخدام express-rate-limit
CORS النطاقات المدرجة في القائمة البيضاء
الحد الأقصى لحجم نص الطلب express.json({ limit: '1mb' })

استراتيجية الدفاع متعدد الطبقات للأمن: لا يقتصر أمن واجهة برمجة التطبيقات (API) على إجراء واحد، بل هو نظام دفاع متعدد الطبقات — 1. طبقة الشبكة (تشفير HTTPS، جدار الحماية WAF، قائمة العناوين IP المسموح بها)؛ 2. طبقة التطبيق (المصادقة + التفويض + التحقق من صحة المدخلات + تحديد معدل الاستخدام)؛ 3. طبقة البيانات (التحقق من صحة مخطط Mongoose + $jsonSchema كخيار احتياطي + select: false لإخفاء الحقول الحساسة)؛ 4. طبقة العمليات (تدقيق السجلات + الكشف عن الحالات الشاذة + الحظر التلقائي). توفر كل طبقة حماية مستقلة؛ فحتى في حالة اختراق إحدى الطبقات، تظل الطبقات الأخرى سليمة. ومن الأخطاء الشائعة الاعتماد على طبقة واحدة فقط (مثل تطبيق المصادقة دون التفويض، أو إجراء التحقق من صحة المخطط دون التحقق من صحة المدخلات).

استراتيجيات تصميم تحديد معدل الطلبات: يجب تكييف استراتيجيات تحديد معدل الطلبات لـ express-rate-limit وفقًا لسيناريوهات محددة — 1. تحديد معدل الطلبات بشكل عام (مشترك بين جميع واجهات برمجة التطبيقات، على سبيل المثال، 100 طلب في الدقيقة لمنع هجمات DDoS)؛ 2. تحديد معدل صارم لنقاط النهاية التي تمت مصادقتها (5 طلبات تسجيل دخول/تسجيل لكل عنوان IP في الدقيقة لمنع هجمات القوة الغاشمة)؛ 3. حدود معتدلة على نقاط النهاية الخاصة بالكتابة (30 طلبًا لكل مستخدم في الدقيقة لعمليات الإنشاء/التحديث، لمنع الرسائل غير المرغوب فيها)؛ 4. حدود متساهلة على نقاط النهاية الخاصة بالقراءة (200 طلب لكل مستخدم في الدقيقة لعمليات العرض/التفاصيل، دون حجب أثناء الاستخدام العادي). يُطبق تحديد المعدل بناءً على نهج ثنائي الأبعاد يجمع بين عنوان IP ومعرف المستخدم — حيث لا يؤثر المستخدمون المتعددون الذين يتشاركون نفس عنوان IP على بعضهم البعض، ويظل المستخدم الفردي الذي يغير عناوين IP خاضعًا للحد المحدد.

مبادئ الأمان لتكوين CORS: يحدد تكوين CORS (مشاركة الموارد عبر الأصول) المجالات الأمامية التي يمكنها استدعاء واجهة برمجة التطبيقات (API) — لا تستخدم Access-Control-Allow-Origin: * أبدًا في بيئة الإنتاج. التكوين الصحيح: 1. قائمة بالمجالات المدرجة في القائمة البيضاء (على سبيل المثال، ['https://shop.example.com', 'https://admin.example.com']؛ 2. طرق HTTP المسموح بها (GET/POST/PUT/DELETE؛ لا توجد طرق خاصة بخلاف OPTIONS)؛ 3. رؤوس الطلبات المسموح بها (Content-Type، Authorization)؛ 4. دعم بيانات الاعتماد (عند استخدام credentials: true، يجب تحديد نطاقات معينة؛ ولا يُسمح باستخدام أحرف البدل). يمكن استخدام حرف البدل * في بيئات التطوير، ولكن يجب تمييزه باستخدام NODE_ENV.

JAVASCRIPT
// routes/reviews.js
const express = require('express');
const router = express.Router();
const Review = require('../models/Review');
const { authenticate } = require('../middlewares/auth');

router.get('/products/:productId/reviews', async (req, res) => {
  const reviews = await Review.find({ productId: req.params.productId })
    .populate('userId', 'username avatar')
    .sort({ createdAt: -1 })
    .limit(20)
    .lean();
  res.json(reviews);
});

router.post('/products/:productId/reviews', authenticate, async (req, res) => {
  const review = await Review.create({
    productId: req.params.productId,
    userId: req.user._id,
    content: req.body.content,
    rating: req.body.rating
  });
  res.status(201).json(review);
});

module.exports = router;

إرفاق البرمجيات الوسيطة على مستوى المسار: يمكن إرفاق البرمجيات الوسيطة في Express على مستويات مختلفة — 1. مستوى التطبيق (app.use(cors()))، والذي ينطبق على جميع المسارات؛ 2. مستوى المسار (router.use(authenticate))، والذي ينطبق على جميع نقاط النهاية التابعة لذلك المسار؛ 3. مستوى نقطة النهاية (router.post('/', authenticate, validate, ctrl.create))، والذي ينطبق فقط على نقطة النهاية تلك. كلما كانت الدقة أعلى، زادت دقة التحكم، ولكن زاد تكرار الكود أيضًا. الاستراتيجية الموصى بها: ضع البرامج الوسيطة العامة (cors/json/logging) على مستوى التطبيق، والمصادقة على مستوى الموجه، والتحقق من الصحة والتفويض على مستوى نقطة النهاية.

معالجة الأخطاء غير المتزامنة في وحدات التحكم: لا يقوم Express 4.x بالتقاط الاستثناءات تلقائيًا في الدوال غير المتزامنة — فإذا تم إلقاء خطأ بواسطة عبارة await داخل دالة غير متزامنة في وحدة التحكم، فلن يقوم Express باستدعاء next(err)، وسيتوقف الطلب عن العمل. ثلاثة حلول: 1. express-async-errors (يحل المشكلة بشكل شامل بسطر واحد من require؛ موصى به بشدة)؛ 2. التغليف يدويًّا باستخدام try-catch + next(err) (مكرر ولكنه صريح)؛ 3. استخدام الدالة ذات الترتيب الأعلى wrapAsync(fn) للتغليف التلقائي (مرن ولكنه يتطلب التغليف اليدوي لكل مسار). الحل 1 غير تدخلي؛ بمجرد تثبيته، تكتسب جميع المسارات غير المتزامنة تلقائيًّا قدرات انتشار الأخطاء.

الاستدعاءات المتسلسلة في منشئ الاستعلامات: يدعم منشئ الاستعلامات في Mongoose الاستدعاءات المتسلسلة — Model.find(query).select(fields).populate(ref).sort(order).skip(n).limit(m).lean(). لا يؤثر ترتيب الاستدعاءات المتسلسلة على نتائج الاستعلام (يقوم Mongoose بتحسين ذلك داخليًا)، لكن الترتيب الأكثر قابلية للقراءة هو: find → select → populate → sort → skip → limit → lean. يتوافق هذا الترتيب مع منطق «تحديد ما سيتم الاستعلام عنه أولاً، ثم تحديد كيفية عرضه». يجب دائمًا وضع lean() في النهاية — لأنها تحول نتائج الاستعلام من مستند Mongoose إلى كائن JavaScript عادي، وبعد ذلك لا يمكنك ربط طرق المستند.

أداء العمليات الجماعية: عندما تحتاج إلى إنشاء سجلات متعددة أو تحديثها أو حذفها، تكون العمليات الجماعية أسرع بـ 10 إلى 100 مرة من العمليات الفردية — 1. تقوم الدالة Model.insertMany([...]) بإدراج N سجلًا في رحلة ذهاب وإياب واحدة، وهو ما يجعلها أسرع بـ N مرة من إجراء N استدعاءات لدالة Model.create(); 2. تقوم Model.updateMany(filter, update) بتحديث جميع المستندات المطابقة في عملية واحدة، وهو أسرع بكثير من تنفيذ عمليات updateOne الفردية؛ 3. تقوم Model.deleteMany(filter) بإجراء الحذف الجماعي. ومع ذلك، فإن العمليات المجمعة لها القيود التالية: 1. لا تُشغّل برامج الوسيطة قبل الحفظ أو بعده (يتم تشغيل برنامج الوسيطة validate فقط أثناء insertMany)؛ 2. لا تُرجع كائنات مستندات كاملة (يتم إرجاع إقرارات الكتابة فقط)؛ 3. يقتصر حجم البيانات لكل عملية على 16 ميغابايت وفقًا لـ BSON.


▶ المثال 1: الاتصالات الأساسية والتوجيه باستخدام Express وMongoose

JAVASCRIPT
// === 1. Minimum Express + mongoose Applications ===
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// mongoose Connect
mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000
}).then(() => console.log('✅ MongoDB connected'))
  .catch(err => { console.error('❌ Connection failed:', err.message); process.exit(1); });

// The simplest CRUD
app.get('/api/products', async (req, res) => {
  const products = await mongoose.model('Product').find().lean();
  res.json({ data: products });
});

app.post('/api/products', async (req, res) => {
  const product = await mongoose.model('Product').create(req.body);
  res.status(201).json({ data: product });
});

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

app.listen(3000, () => console.log('🚀 Server running on port 3000'));

الناتج: تطبيق بسيط وقابل للتشغيل باستخدام Express وMongoose، يمكن تشغيله بثلاثة ملفات فقط (app.js و.env وpackage.json).

▶ المثال 2: بنية كاملة لواجهة برمجة تطبيقات التجارة الإلكترونية باستخدام Express وMongoose

JAVASCRIPT
// === Complete Project Structure ===
// shophub-api/
// ├── src/
// │   ├── app.js              # Express App Portal
// │   ├── config/
// │   │   ├── db.js          # mongoose Connect
// │   │   └── index.js       # Environment Configuration
// │   ├── models/
// │   │   ├── User.js
// │   │   ├── Product.js
// │   │   └── Order.js
// │   ├── controllers/
// │   │   ├── authController.js
// │   │   ├── productController.js
// │   │   └── orderController.js
// │   ├── routes/
// │   │   ├── auth.js
// │   │   ├── products.js
// │   │   └── orders.js
// │   ├── middlewares/
// │   │   ├── auth.js        # JWT Verification
// │   │   ├── errorHandler.js
// │   │   └── validate.js    # joi Verification
// │   └── utils/
// │       └── logger.js
// ├── .env
// └── package.json

// === 1. db.js - Connection Configuration ===
const mongoose = require('mongoose');

async function connectDB() {
  const conn = await mongoose.connect(process.env.MONGODB_URI, {
    serverSelectionTimeoutMS: 5000,
    maxPoolSize: 50,
    minPoolSize: 5,
    socketTimeoutMS: 45000,
    autoIndex: process.env.NODE_ENV !== 'production'  // Production Shutdown autoIndex
  });
  console.log(`✅ MongoDB connected: ${conn.connection.host}`);
  return conn;
}

module.exports = { connectDB };

// === 2. middlewares/auth.js - JWT Verification ===
const jwt = require('jsonwebtoken');

exports.authenticate = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'No token' });
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

exports.authorize = (...roles) => (req, res, next) => {
  if (!req.user || !roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

// === 3. controllers/productController.js ===
const Product = require('../models/Product');

exports.list = async (req, res, next) => {
  try {
    const { page = 1, limit = 20, category, search, sort = 'createdAt', order = 'desc' } = req.query;
    const query = { isActive: true };
    if (category) query.category = category;
    if (search) query.title = new RegExp(search, 'i');

    const [products, total] = await Promise.all([
      Product.find(query)
        .select('sku title price thumbnail rating')
        .sort({ [sort]: order === 'desc' ? -1 : 1 })
        .skip((page - 1) * limit)
        .limit(+limit)
        .lean(),
      Product.countDocuments(query)
    ]);

    res.json({
      success: true,
      data: products,
      meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
    });
  } catch (err) { next(err); }
};

exports.get = async (req, res, next) => {
  try {
    const product = await Product.findOne({ sku: req.params.sku }).lean();
    if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
    res.json({ success: true, data: product });
  } catch (err) { next(err); }
};

exports.create = async (req, res, next) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json({ success: true, data: product });
  } catch (err) { next(err); }
};

// === 4. routes/products.js ===
const router = require('express').Router();
const ctrl = require('../controllers/productController');
const { authenticate, authorize } = require('../middlewares/auth');

router.get('/', ctrl.list);
router.get('/:sku', ctrl.get);
router.post('/', authenticate, authorize('admin'), ctrl.create);

module.exports = router;

// === 5. app.js - Main App ===
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./config/db');

const app = express();
app.use(express.json({ limit: '1mb' }));

// Health Checkup
app.get('/healthz', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

// Routing
app.use('/api/products', require('./routes/products'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/auth', require('./routes/auth'));

// Error Handling
app.use(require('./middlewares/errorHandler'));

(async () => {
  await connectDB();
  app.listen(process.env.PORT || 3000, () => {
    console.log(`🚀 Server running on port ${process.env.PORT || 3000}`);
  });
})();

الناتج: بنية MVC كاملة تدعم المصادقة باستخدام JWT، وأذونات RBAC، ومعالجة الأخطاء، وفحوصات الحالة.

تطور بنية MVC: يمكن تقسيم التطور المعماري للمشروع من مرحلة صغيرة إلى كبيرة إلى ثلاث مراحل — 1. مرحلة الملف الواحد (MVP): يتم تضمين جميع المنطق في app.js؛ مناسبة للعروض التوضيحية والنماذج الأولية (< 100 سطر من التعليمات البرمجية)؛ 2. مرحلة الطبقات (الإنتاج): فصل ملفات النموذج (Model) والمتحكم (Controller) والمسار (Route) + البرمجيات الوسيطة + التكوين؛ مناسبة لمشاريع الإنتاج (100–1,000 سطر)؛ 3. مرحلة الوحدات النمطية (التوسع): يتم تنظيم الوحدات النمطية حسب المجال التجاري (على سبيل المثال، user، order، product، حيث تحتوي كل منها على النموذج + وحدة التحكم + المسار)، مع التواصل بين الوحدات النمطية عبر طبقة الخدمة؛ وهي مناسبة للمشاريع واسعة النطاق (1,000+ سطر). المبدأ الأساسي لهذا التطور هو «إدخال التعقيد فقط عند ظهور نقاط الضعف» — لا تصمم بنية معيارية خلال مرحلة النموذج القابل للتطبيق (MVP)، ولا تستمر في استخدام بنية ملف واحد بمجرد تجاوز قاعدة الكود 1,000 سطر.

قائمة التحقق قبل إطلاق بيئة الإنتاج: يجب التحقق منها قبل نشر التطبيق — 1. متغيرات البيئة: تم تعيين جميع المتغيرات الضرورية وهي صالحة (MONGODB_URI، JWT_SECRET، NODE_ENV=production)؛ 2. اتصال قاعدة البيانات: تم تكوين تجمع الاتصالات بشكل صحيح (تم تعيين maxPoolSize بناءً على التزامن)، وتم تعيين مهلة الانتظار (serverSelectionTimeoutMS: 5000)؛ 3. البرامج الوسيطة الأمنية: helmet (رؤوس الأمان)، cors (قائمة بيضاء عبر الأصول)، express-rate-limit (تحديد المعدل)، mongo-sanitize (الحماية من الحقن)؛ 4. التسجيل: تم تكوين Winston وMorgan بشكل صحيح، ويتم كتابة سجلات الأخطاء في ملف بدلاً من إخراجها إلى وحدة التحكم فقط؛ 5. الإغلاق السلس: يتم التعامل مع إشارات SIGTERM وSIGINT، ولا تنتهي العملية إلا بعد إغلاق اتصال قاعدة البيانات؛ 6. فحص الحالة: يمكن لموزع الحمل اكتشاف نقطة النهاية /health. لا يمكن تشغيل التطبيق إلا بعد اجتياز جميع الفحوصات الستة.

▶ مثال 3: تطبيق Express + Mongoose كامل مع فحص الصحة والإغلاق السلس(الصعوبة ⭐⭐)

JAVASCRIPT
// تطبيق ShopHub الكامل للإنتاج
const express = require('express');
const mongoose = require('mongoose');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');

const app = express();

// === 1. البرمجيات الوسيطة الأساسية ===
app.use(helmet());                    // رؤوس الأمان
app.use(cors({ origin: process.env.CORS_ORIGIN }));
app.use(express.json({ limit: '1mb' }));
app.use(morgan('combined'));

// === 2. فحص الصحة ===
app.get('/healthz', async (req, res) => {
  try {
    await mongoose.connection.db.admin().command({ ping: 1 });
    res.json({
      status: 'healthy',
      mongo: 'connected',
      uptime: Math.floor(process.uptime()),
      timestamp: new Date().toISOString()
    });
  } catch (err) {
    res.status(503).json({
      status: 'unhealthy',
      error: 'database_unavailable'
    });
  }
});

// === 3. المسارات ===
app.use('/api/products', require('./routes/products'));
app.use('/api/users', require('./routes/users'));

// === 4. معالجة الأخطاء ===
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR' } });
});

// === 5. الإغلاق السلس ===
process.on('SIGTERM', async () => {
  console.log('⏹️  إغلاق التطبيق...');
  await mongoose.connection.close();
  console.log('✅ تم إغلاق اتصال قاعدة البيانات');
  process.exit(0);
});

// === 6. بدء التشغيل ===
const start = async () => {
  await mongoose.connect(process.env.MONGODB_URI, {
    maxPoolSize: 50,
    serverSelectionTimeoutMS: 5000
  });
  console.log('✅ MongoDB متصل');

  app.listen(process.env.PORT || 3000, () => {
    console.log(`🚀 الخادم يعمل على المنفذ ${process.env.PORT || 3000}`);
  });
};

start().catch(err => {
  console.error('❌ فشل البدء:', err);
  process.exit(1);
});

الإخراج:

TEXT 📖 للعرض فقط
✅ MongoDB متصل
🚀 الخادم يعمل على المنفذ 3000
[عند طلب /healthz]:
{"status":"healthy","mongo":"connected","uptime":120,"timestamp":"2026-07-20T10:00:00.000Z"}

❓ أسئلة شائعة

س أيهما أفضل، Express أم Koa/Fastify؟
ج يتمتع Express بنظام بيئي أكثر نضجًا. يقدم Fastify أداءً أفضل (2–3 أضعاف)، في حين أن Koa أكثر خفةً.
س ما هو الحجم المناسب لمجمع اتصالات Mongoose؟
ج اضبطه وفقًا لمعدل التزامن. عمومًا، يتراوح بين 10 و50. إن تعيين maxPoolSize بقيمة عالية جدًّا سيؤدي إلى استنفاد اتصالات قاعدة البيانات.
س هل يُعد dotenv آمنًا؟
ج لا بأس به في مرحلة التطوير. أما في مرحلة التشغيل، فنوصي بقراءة القيم من متغيرات البيئة أو من خدمة إدارة الأسرار (مثل AWS Secrets Manager أو Vault).

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): قم بإعداد هيكل أساسي لمشروع يستخدم Express و Mongoose، بما في ذلك مكتبة connectDB والتوجيه الأساسي.
  2. الأسئلة الأساسية (⭐): تنفيذ برمجيات وسيطة لمعالجة الأخطاء (مع التمييز بين ValidationError و CastError و 11000).
  3. تمرين متقدم (⭐⭐): قم بتنفيذ واجهة برمجة تطبيقات (API) كاملة لعمليات CRUD (المنتجات)، بما في ذلك ترقيم الصفحات والتصفية وعمليات الإسقاط.
  4. تمرين متقدم (⭐⭐): استخدم dotenv لإدارة متغيرات البيئة والتمييز بين إعدادات التطوير والإنتاج.
  5. التحدي (⭐⭐⭐): إنجاز واجهة برمجة تطبيقات (API) لنظام التعليقات (GET/POST/PUT/DELETE + برمجيات وسيطة للمصادقة + معالجة الأخطاء).
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%