Node.js: مشروع API (الجزء 3)
آخر تحديث: 2026-08-26
1. وضع اللمسات الأخيرة والإطلاق: اليوم الثالث لأليس
في اليوم الثالث، قامت أليس بكتابة الاختبارات ووثائق واجهة برمجة التطبيقات (API)، بينما قام بوب بإعداد عملية النشر باستخدام دوكر. قال بوب: «مجرد أن الكود يعمل لا يعني أنه جاهز للنشر. فالاختبارات تضمن الجودة، والوثائق تضمن سهولة الصيانة، ودوكر يضمن الاتساق».
- يُعد «Jest + Supertest» المزيج القياسي لاختبار واجهات برمجة التطبيقات (APIs) الخاصة بـ Node.js
- يضمن التعامل الموحد مع الأخطاء اتساق تنسيقات استجابات واجهة برمجة التطبيقات (API)
- يقوم Swagger بإنشاء الوثائق تلقائيًا؛ فالكود هو الوثيقة بحد ذاته.
- تضمن تقنية الحاويات في دوكر (Docker) الاتساق بين بيئات التطوير والإنتاج
- تُعد الفحوصات الصحية من الإعدادات الإلزامية لعمليات النشر في بيئة الإنتاج
2. الاختبار باستخدام Jest و Supertest
(1) اختبار بنية الملف
| الملف | الوصف |
|---|---|
tests/setup.js |
تهيئة بيئة الاختبار (الاتصال بقاعدة بيانات الاختبار) |
tests/auth.test.js |
اختبار وحدة المصادقة |
tests/tasks.test.js |
اختبار وحدة المهام |
tests/helpers.js |
وظائف مساعدة للاختبار (إنشاء مستخدمين للاختبار، وما إلى ذلك) |
▶ مثال: تكوين بيئة الاختبار
JAVASCRIPT
// tests/setup.js
process.env.JWT_SECRET = 'test-secret';
process.env.MONGO_URI = 'mongodb://localhost:27017/task-manager-test';
const mongoose = require('mongoose');
beforeAll(async () => await mongoose.connect(process.env.MONGO_URI));
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
▶ مثال: اختبار وحدة المصادقة
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
describe('Auth API', () => {
beforeEach(غير متزامن () => انتظار User.deleteMany({}));
test('should register a new user', غير متزامن () => {
const res = انتظار request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
expect(res.body.user.username).toBe('alice');
});
test('should login existing user', غير متزامن () => {
انتظار request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
const res = انتظار request(app).post('/api/auth/login').send({
email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
test('should reject invalid credentials', غير متزامن () => {
const res = انتظار request(app).post('/api/auth/login').send({
email: 'noone@test.com', password: 'wrong'
});
expect(res.status).toBe(401);
});
});
▶ مثال: اختبار وحدة المهام
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register').send({
username: 'bob', email: 'bob@test.com', password: '123456'
});
token = reg.body.token;
userId = reg.body.user.id;
});
test('should create a task', async () => {
const res = await request(app).post('/api/tasks').set('Authorization', `Bearer ${token}`).send({ title: 'Write tests' });
expect(res.status).toBe(201);
expect(res.body.title).toBe('Write tests');
});
test('should get tasks list', async () => {
await Task.create({ title: 'Task1', assignedTo: userId });
const res = await request(app).get('/api/tasks').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(1);
});
test('should deny access without token', async () => {
const res = await request(app).get('/api/tasks');
expect(res.status).toBe(401);
});
3. التغليف الموحد لمعالجة الأخطاء
▶ مثال: فئة خطأ مخصصة
JAVASCRIPT
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
module.exports = AppError;
▶ مثال: برمجيات وسيطة عالمية لمعالجة الأخطاء
JAVASCRIPT
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
res.status(statusCode).json({
status: statusCode >= 400 && statusCode < 500 ? 'fail' : 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
- وثائق واجهة برمجة تطبيقات Swagger
(1) علامات التعليقات التوضيحية الشائعة في Swagger
| العلامة | الوصف | الاستخدام |
|---|---|---|
@openapi |
تعريفات المسارات والعمليات | ملفات التوجيه |
@swagger |
تعريف المكون (المخطط) | تكوين الوثائق |
@tags |
مجموعات واجهات برمجة التطبيقات | ملفات التوجيه |
@security |
مرجع طرق المصادقة | نقاط النهاية التي تتطلب المصادقة |
@produces |
تنسيق الاستجابة | ملف التوجيه |
@parameters |
معلمات الطلب | ملف المسار |
▶ مثال: تكوين Swagger
JAVASCRIPT
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0', description: 'Team Task Management RESTful API' },
servers: [{ url: 'http://localhost:3000/api' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = { swaggerUi, specs };
▶ مثال: تعليقات Swagger في المسارات
JAVASCRIPT
/**
* @openapi
* /auth/register:
* post:
* tags: [Auth]
* summary: User Registration
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [username, email, password]
* properties:
* username: { type: string }
* email: { type: string, format: email }
* password: { type: string, minLength: 6 }
* responses:
* 201:
* description: Registration Successful
* 400:
* description: Parameter error
*/
router.post('/register', async (req, res, next) => { /* ... */ });
4. نشر دوكر
(1) وصف ملف Docker
| الملف | الوصف |
|---|---|
Dockerfile |
إنشاء صورة لتطبيق Node.js |
docker-compose.yml |
تطبيق التنسيق + حاوية MongoDB |
.dockerignore |
استبعاد الملفات غير الضرورية مثل node_modules |
(2) قائمة مراجعة إنجاز المشروع
| عنصر الفحص | الحالة |
|---|---|
| يمكن إطلاق المشروع بشكل طبيعي | ☐ |
| واجهة برمجة تطبيقات (API) للتسجيل/تسجيل الدخول متاحة | ☐ |
| تتوفر وظائف CRUD للمهمة | ☐ |
| تتوفر ميزة ترقيم الصفحات والتصفية | ☐ |
| نظام التحكم في الدخول يعمل بشكل طبيعي | ☐ |
| اجتياز الاختبار | ☐ |
| يمكن الوصول إلى وثائق Swagger | ☐ |
| تم إنشاء Docker بنجاح | ☐ |
| نتيجة فحص الحالة: طبيعية | ☐ |
| المعالجة الموحدة للأخطاء | ☐ |
مثال: ملف Dockerfile
DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
▶ مثال: docker-compose.yml
YAML
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"
volumes:
mongo-data:
▶ مثال: نقطة نهاية الفحص الصحي
JAVASCRIPT
router.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
});
- عملية التكامل والتسليم المستمر (CI/CD)
graph LR
A[Code Push] --> B[Run Jest Test]
B -->|Through| C[Build Docker Image]
B -->|Failure| D[Notice to Developers]
C --> E[Push the image to Registry]
E --> F[Deploy to the server]
F --> G[Health Checkup]
G -->|Through| H[Deployment Complete]
G -->|Failure| I[Rollback to a Previous Version]
▶ مثال: ملخص أوامر بدء تشغيل المشروع
BASH
# Development Environment
npm run dev
# Run Test
npm test
# Docker Build
docker-compose up --build
# Production Deployment
docker-compose -f docker-compose.prod.yml up -d
5. مثال شامل: الاختبار + التوثيق + التكوين الكامل لـ Docker
JAVASCRIPT
// tests/tasks.test.js
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register')
.send({ username: 'testuser', email: 'test@test.com', password: '123456' });
token = reg.body.token;
userId = reg.body.user.id;
});
describe('Task API', () => {
test('POST /api/tasks - create task', async () => {
const res = await request(app).post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({ title: 'My Task', priority: 'high' });
expect(res.status).toBe(201);
});
test('GET /api/tasks - list with pagination', async () => {
for (let i = 0; i < 15; i++) {
await Task.create({ title: `Task ${i}`, assignedTo: userId });
}
const res = await request(app).get('/api/tasks?page=2&limit=5')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(5);
expect(res.body.page).toBe(2);
});
test('DELETE /api/tasks/:id - owner can delete', async () => {
const task = await Task.create({ title: 'To Delete', assignedTo: userId });
const res = await request(app).delete(`/api/tasks/${task._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
});
JAVASCRIPT
// src/config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const specs = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0' },
servers: [{ url: '/api' }],
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } } }
},
apis: ['./src/routes/*.js']
});
module.exports = { swaggerUi, specs };
DOCKERFILE
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
YAML
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=change_me_in_prod
depends_on: [mongo]
mongo:
image: mongo:7
volumes: [mongo-data:/data/db]
volumes:
mongo-data:
❓ أسئلة شائعة
س كيف أختار بين Jest و Mocha؟
ج Jest جاهز للاستخدام فورًا دون الحاجة إلى أي إعدادات، ولا يتطلب أي تكوين، ويتضمن أدوات التحقق المدمجة وتغطية الكود؛ أما Mocha فهو أكثر مرونة ولكنه يتطلب استخدام chai أو sinon. بالنسبة لاختبار واجهات برمجة التطبيقات (API) في Node.js، نوصي باستخدام Jest مع Supertest.
س كيف يمكنني اختبار نقاط النهاية التي تتطلب المصادقة في Supertest؟
ج أولاً، قم باستدعاء نقطة النهاية الخاصة بتسجيل الدخول للحصول على رمز المصادقة (token)، ثم قم بتمريره في الطلبات اللاحقة باستخدام .set('Authorization', 'Bearer ' + token).
س هل يتعين عليّ كتابة وثائق Swagger يدويًّا؟
ج يمكنك استخدام
swagger-jsdoc لإنشائها تلقائيًّا من تعليقات JSDoc، أو استخدام swagger-ui-express لعرضها. تعمل التعليقات بمثابة الوثائق، لذا فإن الصيانة المطلوبة ضئيلة للغاية.س كيف يمكنني تحسين حجم صورة Docker؟
ج استخدم الصورة الأساسية Alpine، وعمليات البناء متعددة المراحل (تثبيت التبعيات خلال مرحلة البناء ونسخ النتائج النهائية فقط أثناء وقت التشغيل)، وملف
.dockerignore لاستبعاد الملفات غير الضرورية.س كيف أقوم بإعداد مسار CI/CD؟
ج بالنسبة لمشاريع GitHub، استخدم GitHub Actions: يتم تشغيله عند إجراء عملية «push» → تثبيت التبعيات → تشغيل الاختبارات → إنشاء صورة Docker → إرسالها إلى المستودع → النشر على الخادم.
- س: لماذا ينبغي إنشاء وثائق واجهة برمجة التطبيقات (API) تلقائيًّا؟ ج: قد تتعارض الوثائق التي يتم تحديثها يدويًّا بسهولة مع الكود. تُكتب تعليقات Swagger مباشرةً في الكود، لذا فإن تغيير الكود يؤدي تلقائيًّا إلى تحديث الوثائق، مما يضمن الاتساق.
- س: ما النسبة التي يجب أن تغطيها الاختبارات؟ ج: بالنسبة للمنطق الأساسي للأعمال، نوصي بتغطية ما لا يقل عن 80٪، بما في ذلك المسارات الرئيسية مثل التسجيل، وتسجيل الدخول، وعمليات CRUD، والتحكم في الوصول، بالإضافة إلى الحالات الاستثنائية.
- س: ما هي مزايا النشر باستخدام Docker مقارنةً بالنشر على الأجهزة المادية (bare-metal)؟ ج: اتساق البيئة (التخلص من مشكلة «إنه يعمل على جهازي»)، والنشر السريع، وعزل الموارد، وسهولة التكامل مع CI/CD، والتوسع الأفقي.
- س: ما الذي يجب أن أضعه في اعتباري في بيئة الإنتاج؟ ج: استخدم سرًا قويًّا لقيمة JWT_SECRET، وقم بتمكين المصادقة في MongoDB، وقم بتمكين HTTPS، وقم بتقييد مصادر CORS، وقم بإعداد الحد الأقصى لمعدل الاستخدام، وقم بتكوين جمع السجلات.
- س: كيف يمكنني إجراء فحص الحالة؟ ج: قم بتوفير نقطة النهاية
/api/healthلإرجاع حالة التطبيق وقاعدة البيانات؛ حيث يقوم كل من Docker HEALTHCHECK و livenessProbe في Kubernetes باستدعاء هذه النقطة النهاية بشكل دوري. - س: هل ستتعارض قاعدة بيانات الاختبار مع قاعدة بيانات الإنتاج؟ ج: يستخدم الاختبار قاعدة بيانات منفصلة (مثل
task-manager-test)، ويتم مسح البيانات قبل وبعد كل مجموعة اختبارات، لذا لن يؤثر ذلك على قاعدة بيانات الإنتاج.
📖 ملخص
- الخلاصة والانطلاق: المفاهيم الأساسية لـ «أليس» وكيفية استخدامها في اليوم الثالث
- المفاهيم الأساسية واستخدامات Jest + Supertest في الاختبار
- المفاهيم الأساسية واستخدامات تغليف معالجة الأخطاء الموحدة
- المفاهيم الأساسية واستخدامات وثائق واجهة برمجة التطبيقات (API) الخاصة بـ Swagger
- المفاهيم الأساسية لاستخدام نشر دوكر
- المفاهيم الأساسية وأفضل الممارسات الخاصة بسير عمل التكامل المستمر/التسليم المستمر (CI/CD)
- مثال شامل: المفاهيم الأساسية واستخدامات الاختبار + التوثيق + التكوين الكامل لـ Docker
📝 تمارين
- اكتب ما لا يقل عن 5 حالات اختبار تغطي عمليات المصادقة وعمليات CRUD الخاصة بالمهام، ثم قم بتشغيل
npm testللتأكد من نجاحها جميعًا. - أضف تعليقات Swagger إلى جميع المسارات. بعد تشغيل التطبيق، قم بزيارة
/api-docsللاطلاع على الوثائق. - قم بإنشاء ملفَي Dockerfile و docker-compose.yml، ثم قم بتشغيل
docker-compose up --buildللتحقق من عملية النشر. - أضف نقطة نهاية فحص الحالة
/api/healthوقم بتكوين توجيه HEALTHCHECK في Docker. - استبدل جميع مثيلات
throw new Error()بالفئة المخصصةAppErrorلضمان اتساق تنسيق استجابة الأخطاء.