بناء REST API بـ Express وMongoDB للمبتدئين

في المقال اللي فات جهزنا حساب MongoDB Atlas وعملنا أول اتصال وأول Query. وقبل كده في المقال اللي قبله بنينا سيرفر بـ Express. دلوقتي وقت نوصل الاتنين ببعض ونعمل REST API حقيقي بعمليات CRUD كاملة (إنشاء، قراءة، تحديث، حذف)، بحيث كل route في السيرفر يقرا ويكتب فعليًا في قاعدة البيانات بدل البيانات الوهمية.

المحتويات

أولاً: الـ REST API إيه وإزاي هيكمل المشروع

الـ REST API هو طريقة متعارف عليها لتنظيم السيرفر بحيث كل عملية (إنشاء، قراءة، تحديث، حذف) بيبقى ليها route ومسار (endpoint) واضح، وبتستخدم أفعال الـ HTTP المناسبة: POST للإنشاء، GET للقراءة، PUT أو PATCH للتحديث، وDELETE للحذف. ده بيخلي أي حد يتعامل مع الـ API بتاعك (حتى لو تطبيق موبايل أو موقع تاني) يعرف يتوقع شكل الاستجابة من غير ما يقرا الكود.

لحد دلوقتي عندك جزئين شغالين لوحدهم: سيرفر Express بيرد على طلبات، وقاعدة بيانات MongoDB فيها بيانات. في المقال ده هنخليهم يتكلموا مع بعض، وهيبقى عندك أساس تقدر تبني عليه أي تطبيق حقيقي بعدين.

ثانياً: تجهيز الاتصال وتشغيل السيرفر

أهم فرق عن المقال اللي فات إننا مش هنفتح اتصال جديد بـ MongoDB في كل طلب، هنفتحه مرة واحدة لما السيرفر يشتغل، ونسيب Express يستخدم نفس الاتصال في كل الـ routes.

const express = require("express");
const { MongoClient } = require("mongodb");

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

const uri = "mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority";
const client = new MongoClient(uri);
let usersCollection;

async function startServer() {
  await client.connect();
  const db = client.db("myApp");
  usersCollection = db.collection("users");

  app.listen(3000, () => {
    console.log("السيرفر شغال على البورت 3000");
  });
}

startServer();

الفرق الأساسي: بنعمل connect مرة واحدة جوه startServer، وبنخزن الـ collection في متغير خارجي (usersCollection) عشان أي route بعد كده يقدر يوصله من غير ما يفتح اتصال جديد.

ثالثاً: عملية الإنشاء (Create)

هنضيف أول route بيستقبل بيانات مستخدم جديد ويحفظها في قاعدة البيانات.

app.post("/users", async (req, res) => {
  try {
    const { name, email } = req.body;
    const result = await usersCollection.insertOne({
      name,
      email,
      createdAt: new Date()
    });
    res.status(201).json({ id: result.insertedId });
  } catch (err) {
    res.status(500).json({ error: "حصل خطأ في إنشاء المستخدم" });
  }
});

لاحظ إن الكود لسه async/await زي ما اتعودت من مقال JavaScript، والفرق الوحيد إننا بنستخدم req.body عشان ناخد البيانات اللي جايالنا من الـ client، ورد فعل الخطأ (status 500) لو حصلت مشكلة في قاعدة البيانات نفسها.

رابعاً: عمليات القراءة (Read)

هنعمل route يرجع كل المستخدمين، وroute تاني يرجع مستخدم واحد بمعرفه (ID).

const { ObjectId } = require("mongodb");

app.get("/users", async (req, res) => {
  const users = await usersCollection.find().toArray();
  res.json(users);
});

app.get("/users/:id", async (req, res) => {
  try {
    const user = await usersCollection.findOne({ _id: new ObjectId(req.params.id) });
    if (!user) return res.status(404).json({ error: "المستخدم مش موجود" });
    res.json(user);
  } catch (err) {
    res.status(400).json({ error: "معرف غير صالح" });
  }
});

الـ ObjectId هنا مهم جدًا: معرف المستند في MongoDB مش مجرد نص عادي، لازم تحوله لنوعه الأصلي قبل ما تدور بيه، عشان كده الكود جوه try/catch، لو حد بعت معرف بشكل غلط الكود هيرمي خطأ وهنرجعله رسالة واضحة بدل ما السيرفر يقع.

خامساً: التحديث والحذف (Update وDelete)

نفس فكرة القراءة، هنستخدم الـ ID لتحديد المستند، بس هنغيره أو نمسحه بدل ما نرجعه.

app.put("/users/:id", async (req, res) => {
  try {
    const result = await usersCollection.updateOne(
      { _id: new ObjectId(req.params.id) },
      { $set: req.body }
    );
    if (result.matchedCount === 0) {
      return res.status(404).json({ error: "المستخدم مش موجود" });
    }
    res.json({ message: "تم التحديث" });
  } catch (err) {
    res.status(400).json({ error: "معرف غير صالح" });
  }
});

app.delete("/users/:id", async (req, res) => {
  try {
    const result = await usersCollection.deleteOne({ _id: new ObjectId(req.params.id) });
    if (result.deletedCount === 0) {
      return res.status(404).json({ error: "المستخدم مش موجود" });
    }
    res.json({ message: "تم الحذف" });
  } catch (err) {
    res.status(400).json({ error: "معرف غير صالح" });
  }
});

استخدمنا PUT للتحديث وDELETE للحذف، وده متماشي مع اتفاقية الـ REST اللي اتكلمنا عنها في الأول. لاحظ إن كل عملية بترجع رسالة واضحة سواء نجحت أو فشلت، ده بيسهل جدًا على أي حد هيستخدم الـ API بعدين يعرف يتعامل مع النتيجة.

سادساً: فلترة النتائج وترقيم الصفحات

لو عدد المستخدمين كبر، مش منطقي إنك ترجع كل الصفوف مرة واحدة في كل طلب. Express بياخد أول route مطابق بس ويسيب الباقي، فلازم تستبدل بلوك GET /users اللي عملناه في قسم القراءة بالنسخة دي بالكامل، مش تضيفها كـ route جديد جنبها، وإلا الكود القديم هو اللي هيفضل شغال والتعديل ده مش هيتنفذ خالص.

app.get("/users", async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;

  const users = await usersCollection
    .find()
    .skip(skip)
    .limit(limit)
    .toArray();

  res.json({ page, limit, users });
});

دلوقتي تقدر تطلب /users?page=2&limit=5 وهترجعلك الصفحة التانية بس، 5 مستخدمين فيها. الترتيب هنا مهم: skip بتتخطى عدد معين من المستندات، وlimit بتحدد أقصى عدد ترجعه، والاتنين مع بعض بيديك أي صفحة عايزها من غير ما تحمّل الداتا كلها من قاعدة البيانات.

سابعاً: تجربة الـ API بـ curl

مش لازم تستخدم أداة زي Postman عشان تجرب الـ endpoints، تقدر تجربهم كلهم من الـ Terminal مباشرة بأمر curl (متاح افتراضيًا على macOS وLinux، وعلى Windows لو عندك نسخة حديثة).

# إنشاء مستخدم جديد
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "أحمد", "email": "ahmed@example.com"}'

# جلب أول صفحة، 5 مستخدمين في الصفحة
curl "http://localhost:3000/users?page=1&limit=5"

# تحديث مستخدم معين (استبدل <id> بالمعرف الحقيقي)
curl -X PUT http://localhost:3000/users/<id> \
  -H "Content-Type: application/json" \
  -d '{"name": "أحمد علي"}'

# حذف مستخدم معين
curl -X DELETE http://localhost:3000/users/<id>

جرب الأوامر دي بالترتيب: اعمل مستخدم، هات الصفحة الأولى وشوفه فيها، عدله، وبعدين امسحه وتأكد إنه اختفى من نتيجة القراءة. التجربة اليدوية دي هتفهمك الـ API بشكل عملي أسرع من أي شرح نظري.

ثامناً: أخطاء شائعة هتقابلك

  • نسيان app.use(express.json()) في الأول، وده بيخلي req.body يفضل undefined مهما بعتت بيانات صح.
  • استخدام الـ ID القادم من الرابط (req.params.id) مباشرة من غير تحويله لـ ObjectId، وده بيدي نتيجة فاضية بدل خطأ واضح أحيانًا.
  • الرد بنفس رسالة الخطأ لكل الحالات (زي “error” بس من غير تفاصيل)، وده بيصعب عليك تعرف المشكلة فين وقت التصحيح.
  • عدم التحقق من وجود المستند قبل التحديث أو الحذف، فبتفتكر العملية نجحت وهي أصلاً ملقتش حاجة تعدلها.

تاسعاً: خطوة قبل النشر: التحقق من البيانات ومعالجة الأخطاء

الكود اللي فوق شغال، بس في مشروع حقيقي محتاج طبقة تحقق (validation) قبل ما البيانات توصل لقاعدة البيانات أصلاً، مثلًا التأكد إن الإيميل شكله صح والاسم مش فاضي. مكتبات زي Joi أو express-validator بتسهل الموضوع ده كتير بدل ما تكتب شروط يدوية لكل حقل.

حاجة تانية مهمة: بدل ما تكرر نفس بلوك try/catch في كل route، تقدر تعمل middleware مركزي للأخطاء في Express يستقبلها كلها في مكان واحد. لو مش متعود على فكرة الـ middleware، شرحتها بالتفصيل في مقال Node.js وExpress.js.

وقبل ما تنشر المشروع فعليًا، ارجع لنصايح الحماية اللي ذكرناها في مقال MongoDB Atlas، وحط بيانات الاتصال في ملف .env، وحدد عناوين الـ IP المسموح لها بالاتصال بدل السماح للجميع.

عاشراً: نظّم الكود بأسلوب Clean Code

لو بصيت على كل الأكواد اللي كتبناها، هتلاحظ إن نفس النمط بيتكرر في كل route: تحويل الـ ID لـ ObjectId جوه try/catch، والرد بنفس شكل رسالة الخطأ لو فشل. التكرار ده أول علامة إنك محتاج تطبق مبادئ الـ Clean Code اللي شرحتها في مقال أهمية الـ Clean Code، وأهمها هنا مبدأ DRY (متكررش نفس المنطق).

بدل ما تكتب نفس التحويل والتحقق في كل route، استخرجه في دالة صغيرة منفصلة واستخدمها في كل مكان.

// دالة مساعدة بتحول الـ ID وترجع null لو غلط بدل ما ترمي استثناء
function toObjectId(id) {
  try {
    return new ObjectId(id);
  } catch {
    return null;
  }
}

app.get("/users/:id", async (req, res) => {
  const objectId = toObjectId(req.params.id);
  if (!objectId) return res.status(400).json({ error: "معرف غير صالح" });

  const user = await usersCollection.findOne({ _id: objectId });
  if (!user) return res.status(404).json({ error: "المستخدم مش موجود" });

  res.json(user);
});

لاحظ الفرق: الـ route بقى أقصر وأوضح، وبيقرا كأنه جملة عربية طبيعية (حوّل المعرف، لو غلط ارجع خطأ، هات المستخدم، لو مش موجود ارجع خطأ، رجّعه). لو غيرت طريقة التحقق من الـ ID بعدين، هتعدلها في مكان واحد بس بدل ما تدور عليها في كل route.

حادي عشر: فكّر في التوسع زي أي System Design صح

الـ API اللي بنيناه دلوقتي شغال كويس لمشروع صغير أو متوسط، بس مقال System Design اللي شرحته قبل كده بيقول حاجة مهمة: التفكير في البنية بيبدأ قبل ما تكتب أول سطر كود، مش بعد ما التطبيق يكبر ويبدأ يقع.

لو حبيت تطبق نفس الفكرة على المشروع ده، فكر في النقط دي بدري:

  • كل الكود دلوقتي في ملف واحد، مشروع حقيقي محتاج تفصل الـ routes عن منطق التعامل مع قاعدة البيانات (طبقة اسمها عادة controllers أو services)، عشان لو غيرت قاعدة البيانات نفسها بعدين متعملش تعديل في كل مكان.
  • اتصال واحد بـ MongoDB كويس لسيرفر واحد، لكن لو مشروعك كبر واحتجت أكتر من نسخة من السيرفر شغالة مع بعض (horizontal scaling)، هتحتاج تفكر في إدارة الاتصالات بشكل مختلف.
  • الأخطاء دلوقتي بترجع كنص عادي، مشروع حقيقي محتاج نظام logging بيسجل كل خطأ في مكان تقدر تراجعه بعدين، مش بس يرجع للمستخدم.

مش لازم تطبق كل ده من أول يوم، بس أهم حاجة إنك تعرف الأسئلة دي موجودة وتفكر فيها بدري بدل ما تتفاجئ بيها لما المشروع يكبر.

دلوقتي عندك REST API كامل بيتكلم فعليًا مع قاعدة بيانات حقيقية، ده الأساس اللي أي تطبيق MERN حقيقي مبني عليه. الخطوة الطبيعية بعد كده هي إنك تبني واجهة React تستهلك الـ API ده وتعرض البيانات للمستخدم. لو حابب مقال يشرح الجزء ده، قولّي في التعليقات.


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك رد

Scroll to Top