بناء مشروع REST API - الجزء الثالث
بناء مشروع REST API كامل - الجزء الثالث
في هذا الدرس الأخير من سلسلة مشروع REST API، سنضيف اختباراً شاملاً، وتوثيق API، وتقوية أمان محسنة، ونشر تطبيقنا. سيحول هذا مشروعنا إلى واجهة برمجة تطبيقات REST جاهزة للإنتاج.
الخطوة 1: إعداد بيئة الاختبار
تثبيت تبعيات الاختبار:
npm install --save-dev jest supertest @types/jest
npm install --save-dev mongodb-memory-server
تحديث package.json بنصوص الاختبار:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest --watchAll --verbose",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"]
}
الخطوة 2: إعداد الاختبار
إنشاء ملف إعداد الاختبار (tests/setup.js):
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
// الاتصال بقاعدة البيانات في الذاكرة قبل الاختبارات
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
// مسح قاعدة البيانات بين الاختبارات
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
// قطع الاتصال وإيقاف قاعدة البيانات في الذاكرة بعد الاختبارات
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
module.exports = { mongoServer };
الخطوة 3: اختبارات المصادقة
إنشاء اختبارات المصادقة (tests/auth.test.js):
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
require('./setup');
describe('نقاط نهاية المصادقة', () => {
describe('POST /api/auth/register', () => {
it('يجب تسجيل مستخدم جديد بنجاح', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'محمد أحمد',
email: 'john@example.com',
password: 'password123'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user).toHaveProperty('email', 'john@example.com');
expect(response.body.data).toHaveProperty('token');
expect(response.body.data.user).not.toHaveProperty('password');
});
it('يجب أن يفشل مع بريد إلكتروني مكرر', async () => {
await User.create({
name: 'فاطمة علي',
email: 'jane@example.com',
password: 'password123'
});
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'مستخدم آخر',
email: 'jane@example.com',
password: 'password456'
})
.expect(409);
expect(response.body.success).toBe(false);
});
it('يجب أن يفشل مع بيانات غير صالحة', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'م',
email: 'invalid-email',
password: '123'
})
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await request(app)
.post('/api/auth/register')
.send({
name: 'مستخدم اختبار',
email: 'test@example.com',
password: 'password123'
});
});
it('يجب تسجيل الدخول بنجاح مع بيانات اعتماد صحيحة', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
});
it('يجب أن يفشل مع كلمة مرور غير صحيحة', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
})
.expect(401);
expect(response.body.success).toBe(false);
});
it('يجب أن يفشل مع مستخدم غير موجود', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123'
})
.expect(401);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/auth/profile', () => {
let token;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'مستخدم الملف الشخصي',
email: 'profile@example.com',
password: 'password123'
});
token = response.body.data.token;
});
it('يجب الحصول على ملف تعريف المستخدم مع رمز صالح', async () => {
const response = await request(app)
.get('/api/auth/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('email', 'profile@example.com');
});
it('يجب أن يفشل بدون رمز', async () => {
await request(app)
.get('/api/auth/profile')
.expect(401);
});
it('يجب أن يفشل مع رمز غير صالح', async () => {
await request(app)
.get('/api/auth/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});
الخطوة 4: اختبارات المهام
إنشاء اختبارات المهام (tests/tasks.test.js):
const request = require('supertest');
const app = require('../src/app');
require('./setup');
describe('نقاط نهاية المهام', () => {
let token;
let userId;
beforeEach(async () => {
// تسجيل وتسجيل دخول المستخدم
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'مستخدم المهام',
email: 'taskuser@example.com',
password: 'password123'
});
token = response.body.data.token;
userId = response.body.data.user._id;
});
describe('POST /api/tasks', () => {
it('يجب إنشاء مهمة جديدة', async () => {
const response = await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'مهمة اختبار',
description: 'هذه مهمة اختبار',
status: 'pending',
priority: 'high'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('title', 'مهمة اختبار');
expect(response.body.data).toHaveProperty('userId', userId);
});
it('يجب أن يفشل بدون مصادقة', async () => {
await request(app)
.post('/api/tasks')
.send({
title: 'مهمة اختبار'
})
.expect(401);
});
it('يجب أن يفشل مع بيانات غير صالحة', async () => {
const response = await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'مه' // قصير جداً
})
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/tasks', () => {
beforeEach(async () => {
// إنشاء مهام اختبار
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'مهمة 1',
status: 'pending',
priority: 'high'
});
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'مهمة 2',
status: 'completed',
priority: 'low'
});
});
it('يجب الحصول على جميع المهام', async () => {
const response = await request(app)
.get('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.tasks).toHaveLength(2);
expect(response.body.data).toHaveProperty('pagination');
});
it('يجب تصفية المهام حسب الحالة', async () => {
const response = await request(app)
.get('/api/tasks?status=pending')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.data.tasks).toHaveLength(1);
expect(response.body.data.tasks[0]).toHaveProperty('status', 'pending');
});
it('يجب ترقيم المهام', async () => {
const response = await request(app)
.get('/api/tasks?page=1&limit=1')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.data.tasks).toHaveLength(1);
expect(response.body.data.pagination).toHaveProperty('totalPages', 2);
});
});
describe('GET /api/tasks/statistics', () => {
it('يجب الحصول على إحصائيات المهام', async () => {
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'مهمة اختبار',
status: 'pending'
});
const response = await request(app)
.get('/api/tasks/statistics')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('total', 1);
expect(response.body.data).toHaveProperty('byStatus');
expect(response.body.data.byStatus).toHaveProperty('pending', 1);
});
});
});
mongodb-memory-server لتشغيل الاختبارات في قاعدة بيانات معزولة في الذاكرة. هذا يضمن أن الاختبارات لا تؤثر على قاعدة بيانات التطوير أو الإنتاج الخاصة بك.الخطوة 5: توثيق API باستخدام Swagger
تثبيت تبعيات Swagger:
npm install swagger-jsdoc swagger-ui-express
إنشاء تكوين Swagger (src/config/swagger.js):
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'واجهة برمجة تطبيقات إدارة المهام',
version: '1.0.0',
description: 'واجهة برمجة تطبيقات REST شاملة لإدارة المهام مع المصادقة والفئات ومرفقات الملفات',
contact: {
name: 'دعم API',
email: 'support@taskmanager.com'
},
},
servers: [
{
url: 'http://localhost:5000',
description: 'خادم التطوير'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
User: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['user', 'admin'] },
isActive: { type: 'boolean' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Task: {
type: 'object',
properties: {
_id: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed', 'cancelled']
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'urgent']
},
dueDate: { type: 'string', format: 'date-time' },
categoryId: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
userId: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Error: {
type: 'object',
properties: {
success: { type: 'boolean', example: false },
message: { type: 'string' }
}
}
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = specs;
إضافة Swagger إلى app.js:
const swaggerUi = require('swagger-ui-express');
const swaggerSpecs = require('./config/swagger');
// توثيق API
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
إضافة تعليقات Swagger إلى المسارات (src/routes/auth.js):
/**
* @swagger
* /api/auth/register:
* post:
* summary: تسجيل مستخدم جديد
* tags: [المصادقة]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* example: محمد أحمد
* email:
* type: string
* format: email
* example: john@example.com
* password:
* type: string
* format: password
* minLength: 6
* example: password123
* responses:
* 201:
* description: تم تسجيل المستخدم بنجاح
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* token:
* type: string
* 400:
* description: خطأ في التحقق
* 409:
* description: المستخدم موجود بالفعل
*/
router.post('/register', registerValidation, validate, authController.register);
الخطوة 6: تحديد المعدل
تثبيت وسيط تحديد المعدل:
npm install express-rate-limit
إنشاء وسيط محدد المعدل (src/middleware/rateLimiter.js):
const rateLimit = require('express-rate-limit');
// محدد API العام
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 دقيقة
max: 100, // تحديد كل IP إلى 100 طلب لكل windowMs
message: 'طلبات كثيرة جداً من هذا IP، يرجى المحاولة مرة أخرى لاحقاً'
});
// محدد صارم لمسارات المصادقة
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 محاولات لكل 15 دقيقة
skipSuccessfulRequests: true,
message: 'محاولات مصادقة كثيرة جداً، يرجى المحاولة مرة أخرى لاحقاً'
});
// محدد رفع الملفات
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // ساعة واحدة
max: 10, // 10 عمليات رفع في الساعة
message: 'رفع ملفات كثيرة جداً، يرجى المحاولة مرة أخرى لاحقاً'
});
module.exports = { apiLimiter, authLimiter, uploadLimiter };
تطبيق محددات المعدل في app.js:
const { apiLimiter, authLimiter } = require('./middleware/rateLimiter');
// تطبيق تحديد المعدل
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
الخطوة 7: تسجيل الطلبات
إنشاء مسجل طلبات مخصص (src/middleware/requestLogger.js):
const fs = require('fs');
const path = require('path');
// إنشاء مجلد السجلات إذا لم يكن موجوداً
const logsDir = path.join(__dirname, '../../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
const requestLogger = (req, res, next) => {
const start = Date.now();
// تسجيل الاستجابة
res.on('finish', () => {
const duration = Date.now() - start;
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
};
// التسجيل إلى ملف
const logFile = path.join(logsDir, `${new Date().toISOString().split('T')[0]}.log`);
fs.appendFileSync(logFile, JSON.stringify(log) + '\n');
// تسجيل الأخطاء إلى وحدة التحكم
if (res.statusCode >= 400) {
console.error('خطأ:', log);
}
});
next();
};
module.exports = requestLogger;
الخطوة 8: تعقيم المدخلات
تثبيت حزم التعقيم:
npm install express-mongo-sanitize xss-clean
إضافة وسيط التعقيم إلى app.js:
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
// تعقيم البيانات ضد حقن NoSQL
app.use(mongoSanitize());
// تعقيم البيانات ضد XSS
app.use(xss());
الخطوة 9: التكوين على أساس البيئة
إنشاء ملف التكوين (src/config/config.js):
module.exports = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 5000,
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 10,
maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 5242880,
allowedFileTypes: process.env.ALLOWED_FILE_TYPES?.split(',') || [
'image/jpeg',
'image/png',
'application/pdf'
],
corsOrigin: process.env.CORS_ORIGIN || '*',
logLevel: process.env.LOG_LEVEL || 'info'
};
الخطوة 10: قائمة مراجعة النشر في الإنتاج
إنشاء قائمة مراجعة نشر شاملة:
# متغيرات بيئة الإنتاج
NODE_ENV=production
PORT=5000
MONGODB_URI=mongodb://production-server/taskmanagement
JWT_SECRET=your-production-secret-key-here
JWT_EXPIRES_IN=7d
BCRYPT_ROUNDS=12
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=image/jpeg,image/png,application/pdf
CORS_ORIGIN=https://yourdomain.com
# قائمة مراجعة الإنتاج:
# 1. تعيين JWT_SECRET قوي
# 2. استخدام خادم MongoDB الإنتاج
# 3. تمكين HTTPS
# 4. تكوين CORS لنطاق محدد
# 5. إعداد مدير العمليات (PM2)
# 6. تكوين الخادم الوكيل العكسي (Nginx)
# 7. إعداد المراقبة والتسجيل
# 8. تمكين النسخ الاحتياطية
# 9. تكوين قواعد الجدار الناري
# 10. إعداد شهادات SSL
تثبيت PM2 لإدارة العمليات:
npm install -g pm2
# بدء التطبيق
pm2 start server.js --name task-api
# مراقبة التطبيق
pm2 monit
# عرض السجلات
pm2 logs task-api
# إعادة تشغيل التطبيق
pm2 restart task-api
# حفظ تكوين PM2
pm2 save
# تعيين PM2 للبدء عند تشغيل النظام
pm2 startup
تمرين تطبيقي
أكمل المهام التالية:
- تشغيل جميع الاختبارات والتأكد من نجاحها:
npm test - التحقق من تغطية الاختبار:
npm run test:coverage - الوصول إلى توثيق API على http://localhost:5000/api-docs
- اختبار تحديد المعدل من خلال تقديم طلبات سريعة متعددة
- إضافة توثيق Swagger لمسارات المهام
- اختبار تعقيم المدخلات مع حمولات ضارة
- مراجعة رؤوس الأمان باستخدام أدوات مطور المتصفح
- إعداد PM2 واختبار إعادة تشغيل العملية
ملخص أفضل ممارسات الأمان
تطبق واجهة API الخاصة بنا الآن تدابير أمان شاملة:
- المصادقة: مصادقة قائمة على JWT مع تجزئة كلمة المرور bcrypt
- التفويض: التحكم في الوصول على أساس الدور والتحقق من ملكية المستخدم
- تحديد المعدل: الحماية ضد هجمات القوة الغاشمة و DoS
- التحقق من المدخلات: express-validator لجميع مدخلات المستخدم
- تعقيم المدخلات: الحماية ضد حقن NoSQL و XSS
- رؤوس الأمان: وسيط Helmet لرؤوس أمان HTTP
- CORS: مشاركة الموارد عبر الأصول قابلة للتكوين
- أمان رفع الملفات: التحقق من نوع الملف والحجم
- معالجة الأخطاء: لا يوجد كشف بيانات حساسة في رسائل الخطأ
- التسجيل: تسجيل شامل للطلبات والأخطاء
الخلاصة
في هذا الدرس الأخير، أكملنا مشروع REST API الخاص بنا مع:
- اختبار آلي شامل مع Jest و Supertest
- اختبار قاعدة البيانات في الذاكرة مع mongodb-memory-server
- توثيق API تفاعلي مع Swagger/OpenAPI
- تحديد المعدل للحماية من DDoS والقوة الغاشمة
- تسجيل الطلبات للمراقبة وتصحيح الأخطاء
- تعقيم المدخلات ضد هجمات الحقن
- إدارة التكوين على أساس البيئة
- قائمة مراجعة نشر الإنتاج وإعداد PM2
- تقوية أمان شاملة
لديك الآن واجهة برمجة تطبيقات REST جاهزة للإنتاج مع المصادقة، وعمليات CRUD، ورفع الملفات، والترقيم، والتصفية، والاختبار، والتوثيق، وأفضل ممارسات الأمان. يوضح هذا المشروع مهارات تطوير API على مستوى احترافي تحظى بتقدير كبير في الصناعة.