Node.js: مشروع API (الجزء 2)

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

1. «تركيز الأعمال»: اليوم الثاني لأليس

في اليوم التالي، تولت أليس مسؤولية تنفيذ عمليات CRUD، بينما ركز بوب على التصفية وتقسيم الصفحات والتحكم في الوصول. قالت أليس: «تُعد عمليات CRUD العمود الفقري، والتصفية وتقسيم الصفحات هما تجربة المستخدم، والتحكم في الوصول هو الأمن. وهذه العناصر الثلاثة لا غنى عنها».



2. واجهة برمجة تطبيقات (API) لعمليات إنشاء (CRUD) المهام

(1) تصميم نقاط نهاية CRUD

الطريقة المسار الوصف الأذونات
منشور /api/tasks إنشاء مهمة مستخدم مسجل الدخول
GET /api/tasks الحصول على قائمة المهام المستخدم المسجل
GET /api/tasks/:id استرداد مهمة واحدة أنا أو المسؤول
PUT /api/tasks/:id تحديث المهمة أنا أو المسؤول
حذف /api/tasks/:id حذف المهمة أنا أو المسؤول

▶ مثال: إنشاء مهمة

JAVASCRIPT
router.post('/', auth, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

▶ مثال: استرداد مهمة واحدة

JAVASCRIPT
router.get('/:id', auth, غير متزامن (req, res, next) => {
  try {
    const task = انتظار Task.findById(req.params.id).populate('assignedTo', 'username email');
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo._id.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    res.json(task);
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

▶ مثال: تحديث المهمة

JAVASCRIPT
router.put('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    Object.assign(task, req.body);
    await task.save();
    res.json(task);
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

▶ مثال: حذف مهمة

JAVASCRIPT
router.delete('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    await task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

3. تصفية القوائم، وتقسيم الصفحات، والفرز

(1) وصف معلمات المرشح

المعلمة النوع الوصف مثال
status سلسلة التصفية حسب الحالة ?status=completed
priority سلسلة التصفية حسب الأولوية ?priority=high
assignedTo ObjectId التصفية حسب المكلف (المسؤول) ?assignedTo=userId
dueBefore تاريخ ISO تاريخ الاستحقاق قبل ?dueBefore=2025-12-31
dueAfter تاريخ ISO تاريخ الاستحقاق اللاحق لـ ?dueAfter=2025-01-01
search سلسلة البحث التقريبي حسب العنوان ?search=deploy

(2) معلمات ترقيم الصفحات

المعلمة القيمة الافتراضية الوصف
page 1 الصفحة الحالية
limit 10 العناصر في كل صفحة (بحد أقصى 100)
sort -createdAt حقل الفرز؛ تشير البادئة - إلى الترتيب التنازلي

(3) قواعد الأذونات

الدور نطاق الرؤية نطاق العمل
user مهامي فقط مهامي فقط
admin جميع المهام جميع المهام
user + استعلام assignedTo تجاهل هذا المعامل

▶ مثال: قائمة مقسمة إلى صفحات مع إمكانية التصفية

JAVASCRIPT
router.get('/', auth, غير متزامن (req, res, next) => {
  try {
    const { status, priority, dueBefore, dueAfter, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    else if (req.query.assignedTo) filter.assignedTo = req.query.assignedTo;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (dueBefore || dueAfter) filter.dueDate = {};
    if (dueBefore) filter.dueDate.$lte = new Date(dueBefore);
    if (dueAfter) filter.dueDate.$gte = new Date(dueAfter);
    if (search) filter.タイトル = { $regex: search, $options: 'i' };

    const total = انتظار Task.countDocuments(filter);
    const tasks = انتظار Task.find(filter)
      .populate('assignedTo', 'username email')
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(Number(limit));

    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

▶ مثال: الفرز حسب حقول متعددة

JAVASCRIPT
// ?sort=-priority,createdAt  →  { priority: -1, createdAt: 1 }
const parseSort = (sortStr) => {
  const sortObj = {};
  sortStr.split(',').forEach(field => {
    if (field.startsWith('-')) sortObj[field.slice(1)] = -1;
    else sortObj[field] = 1;
  });
  return sortObj;
};
▶ جرّب الكود

4. البرمجيات الوسيطة للتحقق من صحة البيانات والتفويض

▶ مثال:(1) مسار عمل معالجة الطلبات

100%
graph LR
    A[Client Request] --> B[express-validator]
    B --> C[auth Middleware]
    C --> D[Permission Check]
    D --> E[Business Logic]
    E --> F[Standard Response]
    B -->|Verification Failed| G[400 Error]
    C -->|Not verified| H[401 Error]
    D -->|No permission| I[403 Error]

▶ مثال: قواعد التحقق من الصحة في express-validator

JAVASCRIPT
const { body, query, validationResult } = require('express-validator');

const validateTask = [
  body('title').notEmpty().withMessage('Title is required').isLength({ max: 100 }).withMessage('Title too long'),
  body('status').optional().isIn(['pending', 'in-progress', 'completed']),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601().withMessage('Invalid date format'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
    next();
  }
];
▶ جرّب الكود

▶ مثال: برمجيات وسيطة للتحقق من الأذونات

JAVASCRIPT
const requireAdmin = (req, res, next) => {
  if (req.user.role !== 'admin') return res.status(403).json({ message: 'Admin access required' });
  next();
};

const requireOwnerOrAdmin = (model) => async (req, res, next) => {
  const doc = await model.findById(req.params.id);
  if (!doc) return res.status(404).json({ message: 'Not found' });
  if (doc.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.doc = doc;
  next();
};
▶ جرّب الكود

▶ مثال: حذف المهام دفعة واحدة

JAVASCRIPT
router.delete('/batch', auth, requireAdmin, async (req, res, next) => {
  try {
    const { ids } = req.body;
    const result = await Task.deleteMany({ _id: { $in: ids } });
    res.json({ deleted: result.deletedCount });
  } catch (err) {
    next(err);
  }
});
▶ جرّب الكود

5. مثال شامل: توجيه «المهام» بالكامل

دمج عمليات CRUD والتصفية وتقسيم الصفحات والتحقق من الصحة والأذونات في وحدة توجيه واحدة وشاملة:

JAVASCRIPT
const router = require('express').Router();
const Task = require('../models/Task');
const auth = require('../middleware/auth');
const { body, query, validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
  next();
};

const checkOwner = async (req, res, next) => {
  const task = await Task.findById(req.params.id);
  if (!task) return res.status(404).json({ message: 'Task not found' });
  if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.task = task;
  next();
};

router.post('/', auth, [
  body('title').notEmpty().isLength({ max: 100 }),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601()
], validate, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) { next(err); }
});

router.get('/', auth, async (req, res, next) => {
  try {
    const { status, priority, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (search) filter.title = { $regex: search, $options: 'i' };
    const total = await Task.countDocuments(filter);
    const tasks = await Task.find(filter).populate('assignedTo', 'username').sort(sort).skip((page - 1) * limit).limit(Number(limit));
    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) { next(err); }
});

router.get('/:id', auth, checkOwner, (req, res) => res.json(req.task));

router.put('/:id', auth, checkOwner, [
  body('title').optional().notEmpty().isLength({ max: 100 }),
  body('status').optional().isIn(['pending', 'in-progress', 'completed'])
], validate, async (req, res, next) => {
  try {
    Object.assign(req.task, req.body);
    await req.task.save();
    res.json(req.task);
  } catch (err) { next(err); }
});

router.delete('/:id', auth, checkOwner, async (req, res, next) => {
  try {
    await req.task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) { next(err); }
});

module.exports = router;


❓ أسئلة شائعة

س كيف يتم تنفيذ الاستعلامات المقسمة إلى صفحات؟
ج استخدم skip وlimit: استخدم Task.countDocuments() للحصول على العدد الإجمالي للوثائق، وTask.find().skip((page-1)*limit).limit(limit) لاسترداد بيانات الصفحة الحالية.
س كيف أختار بين express-validator و Joi؟
ج يعتمد express-validator على validator.js ويتكامل بسلاسة مع برامج Express الوسيطة؛ أما Joi فهو أكثر قوة لكنه يتطلب استدعاءً منفصلاً. نوصي باستخدام express-validator لمشاريع Express.
س كيف يمكنني تنفيذ الحذف المؤقت؟
ج أضف حقل deletedAt إلى المخطط وقم بتصفية الاستعلامات باستخدام { deletedAt: null }؛ أو استخدم المكون الإضافي mongoose-delete للتعامل مع ذلك تلقائيًا.
س كيف يتم التحكم في أذونات المهام؟
ج في البرمجيات الوسيطة الخاصة بالتوجيه، قارن بين req.userId وtask.author. لا يمكن إلا للمؤلف تعديل أو حذف مهامه الخاصة؛ أما البقية فيتلقون خطأً برقم 403.
س كيف يمكنني التعامل مع العمليات الجماعية؟
ج استخدم bulkWrite أو updateMany في Mongoose لتنفيذ عدة عمليات كتابة في آن واحد؛ فهذا يوفر أداءً أفضل بكثير من تنفيذ العمليات بشكل فردي باستخدام الحلقات.

📖 ملخص

📝 تمارين

  1. تنفيذ مسارات CRUD كاملة للمهام واستخدام Postman لاختبار عمليات الإنشاء والاستعلام والتحديث والحذف واحدة تلو الأخرى.
  2. إضافة ميزات التصفية وتقسيم الصفحات، واختبار الاستعلامات المركبة مثل ?status=completed&page=2&limit=5&sort=-priority.
  3. اكتب برنامج الوسيط requireAdmin لضمان إرجاع خطأ 403 عندما يحاول مستخدم عادي الوصول إلى واجهة الإدارة.
  4. أضف قواعد التحقق من الصحة باستخدام «express-validator» لعمليات التسجيل وتسجيل الدخول، واختبر استجابات الأخطاء في حالة وجود حقول فارغة أو تنسيقات غير صالحة.

Web-Tutorial.com

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

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

100%