Node.js: مشروع API (الجزء 2)
آخر تحديث: 2026-08-26
1. «تركيز الأعمال»: اليوم الثاني لأليس
في اليوم التالي، تولت أليس مسؤولية تنفيذ عمليات CRUD، بينما ركز بوب على التصفية وتقسيم الصفحات والتحكم في الوصول. قالت أليس: «تُعد عمليات CRUD العمود الفقري، والتصفية وتقسيم الصفحات هما تجربة المستخدم، والتحكم في الوصول هو الأمن. وهذه العناصر الثلاثة لا غنى عنها».
- واجهة برمجة التطبيقات (API) الخاصة بعمليات CRUD هي المنطق الأساسي لإدارة المهام
- يتيح مرشح الاستعلام للمستخدمين تصفية المهام حسب الحالة أو الأولوية أو التاريخ
- يساعد ترقيم الصفحات والفرز على منع تدهور الأداء عند التعامل مع مجموعات البيانات الكبيرة
- تضمن أذونات الأدوار أن المستخدمين العاديين لا يمكنهم العمل إلا على مهامهم الخاصة
- express-validator: يوحد عملية التحقق من صحة تنسيقات معلمات الطلبات
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) مسار عمل معالجة الطلبات
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 لتنفيذ عدة عمليات كتابة في آن واحد؛ فهذا يوفر أداءً أفضل بكثير من تنفيذ العمليات بشكل فردي باستخدام الحلقات.- س: كيف يمكنني تنفيذ تقسيم الصفحات؟ ج: استخدم
.skip((page-1)*limit).limit(limit)في Mongoose لتنفيذ تقسيم الصفحات استنادًا إلى الإزاحة، واستخدمcountDocumentsلإرجاع العدد الإجمالي وحساب إجمالي عدد الصفحات. - س: كيف يمكنني تقييد المستخدمين بحيث يعملون فقط على مهامهم الخاصة؟ ج: أضف
assignedTo: req.user._idإلى مرشح الاستعلام، وقبل التحديث أو الحذف، تحقق مما إذا كانtask.assignedToيساوي معرّف المستخدم الحالي. - س: كيف يمكنني حذف العناصر دفعة واحدة؟ ج: استخدم
Task.deleteMany({ _id: { $in: ids } })، لكننا نوصي بقصر العمليات الجماعية على المستخدمين الذين يحملون دور «admin». - س: كيف يمكنني التعامل مع الترتيب حسب حقول متعددة؟ ج: افصل بينها بفواصل، مثل
?sort=-priority,createdAt، والتي يتم تحليلها على أنها{ priority: -1, createdAt: 1 }وتُمرر إلى Mongoose على أنها.sort(). - س: كيف يمكنني توحيد تنسيق أخطاء التحقق من الصحة؟ ج: استخدم
validationResultمن مكتبة express-validator لضمان اتباع جميع الردود بنية{ errors: [{ msg, param, value }] }. - س: هل هناك مجال لتحسين أداء ترقيم الصفحات؟ ج: عند التعامل مع مجموعات البيانات الكبيرة، تكون عملية
skipبطيئة. يمكنك التبديل إلى ترقيم الصفحات القائم على المؤشر (باستخدام تصفية$gtاستنادًا إلى_idأوcreatedAt) لتجنب تخطي أعداد كبيرة من المستندات.
📖 ملخص
- التركيز على الأعمال: المفاهيم الأساسية واستخدام برنامج «أليس» في اليوم الثاني
- المفاهيم الأساسية واستخدام واجهة برمجة التطبيقات (API) الخاصة بـ CRUD
- المفاهيم الأساسية واستخدامات تصفية القوائم وتقسيم الصفحات والفرز
- المفاهيم الأساسية واستخدامات برمجيات الوسيطة الخاصة بالتحقق من صحة البيانات والتفويض
- مثال شامل: المفاهيم الأساسية واستخدام مسار «tasks» بالكامل
📝 تمارين
- تنفيذ مسارات CRUD كاملة للمهام واستخدام Postman لاختبار عمليات الإنشاء والاستعلام والتحديث والحذف واحدة تلو الأخرى.
- إضافة ميزات التصفية وتقسيم الصفحات، واختبار الاستعلامات المركبة مثل
?status=completed&page=2&limit=5&sort=-priority. - اكتب برنامج الوسيط
requireAdminلضمان إرجاع خطأ 403 عندما يحاول مستخدم عادي الوصول إلى واجهة الإدارة. - أضف قواعد التحقق من الصحة باستخدام «express-validator» لعمليات التسجيل وتسجيل الدخول، واختبر استجابات الأخطاء في حالة وجود حقول فارغة أو تنسيقات غير صالحة.