Node.js و Express
بناء واجهات برمجة التطبيقات RESTful باستخدام Express
بناء واجهات برمجة التطبيقات RESTful باستخدام Express
تعتبر واجهات برمجة التطبيقات RESTful العمود الفقري لتطبيقات الويب الحديثة، حيث تتيح التواصل بين واجهات المستخدم الأمامية والخوادم الخلفية. في هذا الدرس، سنستكشف كيفية تصميم وبناء واجهات برمجة تطبيقات RESTful قوية باستخدام Express.js.
فهم مبادئ REST
REST (نقل الحالة التمثيلية) هو نمط معماري يحدد مجموعة من القيود لإنشاء خدمات الويب. المبادئ الرئيسية تشمل:
- عديم الحالة: كل طلب يحتوي على جميع المعلومات الضرورية؛ الخادم لا يخزن سياق العميل
- فصل العميل والخادم: الواجهة الأمامية والخلفية مستقلتان وتتواصلان عبر HTTP
- واجهة موحدة: يتم الوصول إلى الموارد باستخدام أساليب HTTP القياسية
- قائم على الموارد: كل شيء هو مورد يتم تحديده بواسطة URIs
- قابل للتخزين المؤقت: يمكن تخزين الاستجابات مؤقتًا لتحسين الأداء
ملاحظة: تستخدم واجهات REST أساليب HTTP بشكل دلالي: GET للاسترجاع، POST للإنشاء، PUT/PATCH للتحديثات، وDELETE للحذف.
تصميم نقاط النهاية للـ API
التصميم الجيد لواجهة برمجة التطبيقات يتبع أنماطًا متسقة ويستخدم أساليب HTTP المناسبة:
// بنية نقطة النهاية القائمة على الموارد
GET /api/users // الحصول على جميع المستخدمين
GET /api/users/:id // الحصول على مستخدم واحد
POST /api/users // إنشاء مستخدم جديد
PUT /api/users/:id // تحديث المستخدم بالكامل
PATCH /api/users/:id // تحديث جزئي للمستخدم
DELETE /api/users/:id // حذف مستخدم
// الموارد المتداخلة
GET /api/users/:id/posts // الحصول على منشورات المستخدم
POST /api/users/:id/posts // إنشاء منشور للمستخدم
GET /api/posts/:id/comments // الحصول على تعليقات المنشور
أفضل ممارسة: استخدم أسماء جمع للموارد (users, posts, comments) وتجنب الأفعال في نقاط النهاية. أسلوب HTTP يوفر الفعل.
استجابات JSON وأكواد الحالة
تتواصل واجهات RESTful باستخدام تنسيق JSON وأكواد حالة HTTP المناسبة:
const express = require('express');
const router = express.Router();
// GET جميع المستخدمين - 200 OK
router.get('/users', async (req, res) => {
try {
const users = await User.find();
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
message: 'خطأ في الخادم'
});
}
});
// GET مستخدم واحد - 200 OK أو 404 Not Found
router.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
message: 'خطأ في الخادم'
});
}
});
نمط عمليات CRUD
تنفيذ عمليات CRUD الكاملة (إنشاء، قراءة، تحديث، حذف):
// CREATE - 201 Created
router.post('/users', async (req, res) => {
try {
const { name, email, password } = req.body;
// التحقق من وجود المستخدم
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'المستخدم موجود بالفعل'
});
}
const user = await User.create({ name, email, password });
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
});
// UPDATE - 200 OK
router.put('/users/:id', async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'المستخدم غير موجود'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
});
// DELETE - 204 No Content
router.delete('/users/:id', async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'المستخدم غير موجود'
});
}
res.status(204).send();
} catch (error) {
res.status(500).json({
success: false,
message: 'خطأ في الخادم'
});
}
});
أكواد حالة HTTP:
- 200 OK: نجاح GET, PUT, PATCH
- 201 Created: نجاح POST
- 204 No Content: نجاح DELETE
- 400 Bad Request: بيانات غير صالحة
- 404 Not Found: المورد غير موجود
- 500 Internal Server Error: مشكلة في الخادم
إصدارات API
تتيح الإصدارات تطور واجهة برمجة التطبيقات دون كسر العملاء الحاليين:
// إصدار URL (الأكثر شيوعًا)
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// إصدار الرأس
app.use((req, res, next) => {
const version = req.headers['api-version'] || 'v1';
req.apiVersion = version;
next();
});
// مثال على بنية مُصدَّرة
// routes/v1/users.js
const express = require('express');
const router = express.Router();
router.get('/users', (req, res) => {
res.json({ version: 'v1', data: [] });
});
module.exports = router;
// app.js
const v1Users = require('./routes/v1/users');
const v2Users = require('./routes/v2/users');
app.use('/api/v1', v1Users);
app.use('/api/v2', v2Users);
أفضل ممارسة: استخدم إصدار URL (مثل /api/v1/) لأنه صريح، سهل الاختبار، ويعمل بشكل جيد مع أدوات المتصفح.
التفاوض على المحتوى
التعامل مع تنسيقات الاستجابة المختلفة بناءً على تفضيلات العميل:
router.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
message: 'المستخدم غير موجود'
});
}
// التفاوض على المحتوى بناءً على رأس Accept
res.format({
'application/json': () => {
res.json({
success: true,
data: user
});
},
'application/xml': () => {
res.type('application/xml');
res.send(`<user>
<id>${user.id}</id>
<name>${user.name}</name>
</user>`);
},
'text/html': () => {
res.render('user', { user });
},
'default': () => {
res.status(406).send('غير مقبول');
}
});
} catch (error) {
res.status(500).json({ message: 'خطأ في الخادم' });
}
});
مثال API كامل
// controllers/userController.js
const User = require('../models/User');
exports.getUsers = async (req, res) => {
try {
const { page = 1, limit = 10, sort = 'createdAt' } = req.query;
const users = await User.find()
.limit(limit * 1)
.skip((page - 1) * limit)
.sort(sort);
const count = await User.countDocuments();
res.status(200).json({
success: true,
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
totalPages: Math.ceil(count / limit),
totalItems: count
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
exports.createUser = async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};
// routes/users.js
const express = require('express');
const router = express.Router();
const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
router.route('/')
.get(getUsers)
.post(createUser);
router.route('/:id')
.get(getUser)
.put(updateUser)
.delete(deleteUser);
module.exports = router;
// app.js
const userRoutes = require('./routes/users');
app.use('/api/v1/users', userRoutes);
أخطاء شائعة:
- استخدام الأفعال في أسماء نقاط النهاية (/getUsers بدلاً من GET /users)
- عدم استخدام أكواد حالة HTTP المناسبة
- كشف البيانات الحساسة في الاستجابات
- عدم تنفيذ الترقيم للبيانات الكبيرة
- تنسيقات استجابة غير متسقة
تمرين عملي
ابنِ واجهة برمجة تطبيقات RESTful لنظام مدونة مع المتطلبات التالية:
- إنشاء نقاط نهاية للمنشورات: GET (الكل/واحد)، POST، PUT، DELETE
- تنفيذ أكواد حالة HTTP المناسبة
- إضافة الترقيم والفرز إلى GET جميع المنشورات
- إنشاء مسارات متداخلة للتعليقات: GET /posts/:id/comments
- تنفيذ إصدارات API (/api/v1)
- استخدام تنسيق استجابة JSON متسق مع حقول success و data و message