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

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

1. وضع اللمسات الأخيرة والإطلاق: اليوم الثالث لأليس

في اليوم الثالث، قامت أليس بكتابة الاختبارات ووثائق واجهة برمجة التطبيقات (API)، بينما قام بوب بإعداد عملية النشر باستخدام دوكر. قال بوب: «مجرد أن الكود يعمل لا يعني أنه جاهز للنشر. فالاختبارات تضمن الجودة، والوثائق تضمن سهولة الصيانة، ودوكر يضمن الاتساق».



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 })
  });
};
▶ جرّب الكود
  1. وثائق واجهة برمجة تطبيقات 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() });
});
▶ جرّب الكود
  1. عملية التكامل والتسليم المستمر (CI/CD)
100%
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 → إرسالها إلى المستودع → النشر على الخادم.

📖 ملخص

📝 تمارين

  1. اكتب ما لا يقل عن 5 حالات اختبار تغطي عمليات المصادقة وعمليات CRUD الخاصة بالمهام، ثم قم بتشغيل npm test للتأكد من نجاحها جميعًا.
  2. أضف تعليقات Swagger إلى جميع المسارات. بعد تشغيل التطبيق، قم بزيارة /api-docs للاطلاع على الوثائق.
  3. قم بإنشاء ملفَي Dockerfile و docker-compose.yml، ثم قم بتشغيل docker-compose up --build للتحقق من عملية النشر.
  4. أضف نقطة نهاية فحص الحالة /api/health وقم بتكوين توجيه HEALTHCHECK في Docker.
  5. استبدل جميع مثيلات throw new Error() بالفئة المخصصة AppError لضمان اتساق تنسيق استجابة الأخطاء.

Web-Tutorial.com

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

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

100%