ITSkillsCenter
تطوير الويب

بناء واجهات برمجة التطبيقات REST API باستخدام Node.js و Express: من التصميم إلى النشر

7 min de lecture
صورة توضيحية لبناء واجهات REST API باستخدام Node.js وExpress مع طرق HTTP وبيانات JSON وتوجيه Express

ما هي واجهات برمجة التطبيقات REST API؟

REST API هي الطريقة المعيارية التي تتواصل بها التطبيقات مع بعضها عبر الإنترنت. عندما يفتح تطبيق الطقس على هاتفك، يرسل طلباً لـ API ويحصل على بيانات الطقس. عندما تسجل دخولك في موقع، يتحقق API من بياناتك.

في هذا الدليل، ستبني REST API كاملة باستخدام Node.js و Express مع قاعدة بيانات، من الصفر إلى النشر على الإنترنت.

إعداد بيئة العمل

تثبيت Node.js

# على Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# التحقق من التثبيت
node --version
npm --version

إنشاء المشروع

# إنشاء مجلد المشروع
mkdir api-project
cd api-project

# تهيئة المشروع
npm init -y

# تثبيت المكتبات المطلوبة
npm install express mongoose dotenv cors
npm install -D nodemon

هيكل المشروع

api-project/
├── server.js          # نقطة البداية
├── .env               # متغيرات البيئة
├── .gitignore
├── package.json
├── routes/
│   └── products.js    # مسارات المنتجات
├── models/
│   └── Product.js     # نموذج المنتج
├── middleware/
│   └── errorHandler.js
└── config/
    └── db.js          # اتصال قاعدة البيانات

بناء الخادم الأساسي

ملف server.js

const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./config/db');

// تحميل متغيرات البيئة
dotenv.config();

// الاتصال بقاعدة البيانات
connectDB();

const app = express();

// Middleware
app.use(cors());
app.use(express.json());

// المسارات
app.use('/api/products', require('./routes/products'));

// معالجة الأخطاء
app.use(require('./middleware/errorHandler'));

// تشغيل الخادم
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`الخادم يعمل على المنفذ ${PORT}`);
});

ملف config/db.js

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URI);
    console.log(`MongoDB متصل: ${conn.connection.host}`);
  } catch (error) {
    console.error(`خطأ: ${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

ملف .env

PORT=5000
MONGO_URI=mongodb://localhost:27017/my-store
NODE_ENV=development

إنشاء نموذج البيانات (Model)

ملف models/Product.js

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'اسم المنتج مطلوب'],
    trim: true,
    maxlength: [100, 'الاسم لا يتجاوز 100 حرف']
  },
  description: {
    type: String,
    required: [true, 'الوصف مطلوب'],
    maxlength: [500, 'الوصف لا يتجاوز 500 حرف']
  },
  price: {
    type: Number,
    required: [true, 'السعر مطلوب'],
    min: [0, 'السعر لا يمكن أن يكون سالباً']
  },
  category: {
    type: String,
    enum: ['إلكترونيات', 'ملابس', 'كتب', 'أخرى'],
    default: 'أخرى'
  },
  inStock: {
    type: Boolean,
    default: true
  }
}, {
  timestamps: true // يضيف createdAt و updatedAt تلقائياً
});

module.exports = mongoose.model('Product', productSchema);

بناء المسارات (Routes) — عمليات CRUD

ملف routes/products.js

const express = require('express');
const router = express.Router();
const Product = require('../models/Product');

// GET /api/products - جلب كل المنتجات
router.get('/', async (req, res) => {
  try {
    // دعم الفلترة والترتيب
    const { category, sort, limit = 10, page = 1 } = req.query;
    
    let query = {};
    if (category) query.category = category;
    
    const products = await Product.find(query)
      .sort(sort || '-createdAt')
      .limit(parseInt(limit))
      .skip((parseInt(page) - 1) * parseInt(limit));
    
    const total = await Product.countDocuments(query);
    
    res.json({
      success: true,
      count: products.length,
      total,
      page: parseInt(page),
      pages: Math.ceil(total / parseInt(limit)),
      data: products
    });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// GET /api/products/:id - جلب منتج واحد
router.get('/:id', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ 
        success: false, 
        error: 'المنتج غير موجود' 
      });
    }
    res.json({ success: true, data: product });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// POST /api/products - إضافة منتج جديد
router.post('/', async (req, res) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json({ success: true, data: product });
  } catch (error) {
    if (error.name === 'ValidationError') {
      const messages = Object.values(error.errors).map(e => e.message);
      return res.status(400).json({ success: false, errors: messages });
    }
    res.status(500).json({ success: false, error: error.message });
  }
});

// PUT /api/products/:id - تحديث منتج
router.put('/:id', async (req, res) => {
  try {
    const product = await Product.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!product) {
      return res.status(404).json({ 
        success: false, 
        error: 'المنتج غير موجود' 
      });
    }
    res.json({ success: true, data: product });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// DELETE /api/products/:id - حذف منتج
router.delete('/:id', async (req, res) => {
  try {
    const product = await Product.findByIdAndDelete(req.params.id);
    if (!product) {
      return res.status(404).json({ 
        success: false, 
        error: 'المنتج غير موجود' 
      });
    }
    res.json({ success: true, data: {} });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

module.exports = router;

معالجة الأخطاء

ملف middleware/errorHandler.js

const errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  
  res.status(err.statusCode || 500).json({
    success: false,
    error: err.message || 'خطأ في الخادم'
  });
};

module.exports = errorHandler;

اختبار الـ API

شغّل الخادم ثم اختبر باستخدام curl أو Postman:

# تشغيل الخادم
npm run dev

# إضافة منتج
curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"هاتف ذكي","description":"هاتف حديث","price":500,"category":"إلكترونيات"}'

# جلب كل المنتجات
curl http://localhost:5000/api/products

# جلب منتج واحد
curl http://localhost:5000/api/products/ID_HERE

# تحديث منتج
curl -X PUT http://localhost:5000/api/products/ID_HERE \
  -H "Content-Type: application/json" \
  -d '{"price":450}'

# حذف منتج
curl -X DELETE http://localhost:5000/api/products/ID_HERE

إضافة التحقق من الهوية (Authentication)

# تثبيت المكتبات
npm install bcryptjs jsonwebtoken
// مثال مبسط لتسجيل وتسجيل دخول
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// تسجيل مستخدم جديد
router.post('/register', async (req, res) => {
  const { name, email, password } = req.body;
  
  // تشفير كلمة المرور
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(password, salt);
  
  const user = await User.create({
    name, email, password: hashedPassword
  });
  
  // إنشاء Token
  const token = jwt.sign(
    { id: user._id },
    process.env.JWT_SECRET,
    { expiresIn: '30d' }
  );
  
  res.status(201).json({ success: true, token });
});

// Middleware للحماية
const protect = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'غير مصرح' });
  }
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.id);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Token غير صالح' });
  }
};

// حماية مسار
router.post('/products', protect, createProduct);

نشر الـ API على الإنترنت

يمكنك نشر API مجاناً على عدة منصات:

  • Railway: سهل الاستخدام مع دعم MongoDB
  • Render: خطة مجانية مع نشر تلقائي من GitHub
  • Vercel: مثالي لـ Serverless Functions

التحقق من صحة البيانات مع Joi

التحقق من صحة البيانات المدخلة هو خط الدفاع الأول لحماية تطبيقك من البيانات الخاطئة أو الخبيثة. مكتبة Joi توفر طريقة سهلة وقوية لتعريف مخططات التحقق وتطبيقها على كل طلب يصل إلى API الخاص بك. يمكنك تحديد أنواع البيانات والحدود الدنيا والقصوى والتنسيقات المطلوبة والعلاقات بين الحقول:

const Joi = require("joi");

// مخطط التحقق لإنشاء مستخدم
const createUserSchema = Joi.object({
  name: Joi.string().min(2).max(50).required()
    .messages({ "string.min": "الاسم قصير جداً" }),
  email: Joi.string().email().required()
    .messages({ "string.email": "بريد إلكتروني غير صالح" }),
  password: Joi.string().min(8)
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .required()
    .messages({ "string.pattern.base": "يجب أن تحتوي على أحرف كبيرة وصغيرة وأرقام" }),
  phone: Joi.string().pattern(/^\+?[0-9]{10,15}$/)
    .optional(),
  role: Joi.string().valid("user", "admin").default("user")
});

// Middleware للتحقق
const validate = (schema) => (req, res, next) => {
  const { error } = schema.validate(req.body, { abortEarly: false });
  if (error) {
    const messages = error.details.map(d => d.message);
    return res.status(400).json({
      status: "fail",
      errors: messages
    });
  }
  next();
};

// الاستخدام في المسارات
router.post("/", validate(createUserSchema), createUser);

التعامل مع الملفات والصور

رفع الملفات والصور من المتطلبات الشائعة في أي تطبيق ويب. مكتبة Multer توفر middleware سهل الاستخدام للتعامل مع multipart/form-data في Express. يمكنك تحديد أنواع الملفات المسموحة وحجمها الأقصى ومكان تخزينها:

const multer = require("multer");
const path = require("path");

// إعداد التخزين
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const uniqueName = Date.now() + "-" + Math.round(Math.random() * 1E9);
    cb(null, uniqueName + path.extname(file.originalname));
  }
});

// فلتر نوع الملف
const fileFilter = (req, file, cb) => {
  const allowed = ["image/jpeg", "image/png", "image/webp"];
  if (allowed.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error("نوع ملف غير مسموح"), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 } // 5MB
});

// مسار رفع الصورة
router.post("/upload-avatar",
  protect,
  upload.single("avatar"),
  async (req, res) => {
    const user = await User.findByIdAndUpdate(
      req.user.id,
      { avatar: "/uploads/" + req.file.filename },
      { new: true }
    );
    res.json({ status: "success", data: user });
  }
);

WebSocket للاتصال في الوقت الفعلي

بينما REST API يعتمد على نموذج طلب-استجابة حيث يبدأ العميل كل تفاعل، توفر WebSocket قناة اتصال ثنائية الاتجاه مستمرة بين العميل والخادم. هذا مثالي لتطبيقات الدردشة والإشعارات الفورية ولوحات المعلومات الحية وتحديثات الأسعار في الوقت الفعلي:

const { Server } = require("socket.io");
const http = require("http");
const app = require("./app");

const server = http.createServer(app);
const io = new Server(server, {
  cors: { origin: "http://localhost:5173" }
});

io.on("connection", (socket) => {
  console.log("مستخدم متصل: " + socket.id);

  // الانضمام لغرفة
  socket.on("join-room", (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit("user-joined", socket.id);
  });

  // استقبال وإرسال رسائل
  socket.on("send-message", (data) => {
    io.to(data.room).emit("new-message", {
      text: data.text,
      sender: socket.id,
      timestamp: new Date()
    });
  });

  socket.on("disconnect", () => {
    console.log("مستخدم انقطع: " + socket.id);
  });
});

server.listen(3000);

النشر على خوادم الإنتاج

نشر API على خادم إنتاج يتطلب مراعاة عدة عوامل أمنية وأداءية لضمان عمل التطبيق بشكل موثوق وآمن. إليك الخطوات الأساسية للنشر على خادم VPS مع إعداد Nginx كـ reverse proxy وشهادة SSL مجانية من Let’s Encrypt:

# إعداد Nginx كـ Reverse Proxy
# /etc/nginx/sites-available/api.example.com
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }
}

# تفعيل الموقع
sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# إضافة شهادة SSL مع Certbot
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com

مراقبة وتسجيل الأحداث (Logging)

في بيئة الإنتاج، تحتاج إلى نظام تسجيل موثوق لتتبع الأخطاء ومراقبة أداء التطبيق واكتشاف المشاكل قبل أن تؤثر على المستخدمين. مكتبة Winston توفر نظام تسجيل مرن يدعم مستويات متعددة وأهداف متنوعة للتسجيل:

const winston = require("winston");

const logger = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: "logs/error.log", level: "error" }),
    new winston.transports.File({ filename: "logs/combined.log" })
  ]
});

// في بيئة التطوير أضف الطرفية
if (process.env.NODE_ENV !== "production") {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// Middleware لتسجيل الطلبات
app.use((req, res, next) => {
  logger.info({
    method: req.method,
    url: req.originalUrl,
    ip: req.ip
  });
  next();
});

module.exports = logger;

حماية API من الهجمات الشائعة

أمان API يجب أن يكون أولوية قصوى منذ بداية التطوير وليس فكرة لاحقة. هناك عدة أنواع من الهجمات التي تستهدف واجهات برمجة التطبيقات ويجب الحماية منها بطبقات متعددة من الأمان:

  • حقن SQL و NoSQL: استخدم دائماً معاملات محددة (parameterized queries) ومكتبات ORM مثل Mongoose التي تعالج المدخلات تلقائياً. لا تدخل بيانات المستخدم مباشرة في استعلامات قاعدة البيانات أبداً
  • هجمات XSS: استخدم مكتبة helmet لإعداد رؤوس HTTP الأمنية، ونظّف جميع المدخلات قبل تخزينها أو عرضها. مكتبة express-mongo-sanitize تزيل محاولات حقن MongoDB
  • هجمات CSRF: استخدم رموز CSRF مع ملفات تعريف الارتباط، أو اعتمد على رموز JWT في رأس Authorization بدلاً من الكوكيز
  • هجمات القوة الغاشمة: حدد عدد محاولات تسجيل الدخول الفاشلة وأضف تأخيراً متزايداً بعد كل محاولة فاشلة. استخدم express-rate-limit مع Redis لتحديد المعدل بشكل فعال
  • تسريب البيانات: لا ترسل أبداً كلمات المرور أو الرموز السرية في الاستجابات. استخدم select(« -password ») في Mongoose واختر الحقول المطلوبة فقط
const helmet = require("helmet");
const mongoSanitize = require("express-mongo-sanitize");
const xss = require("xss-clean");
const hpp = require("hpp");

// طبقات الأمان
app.use(helmet());                    // رؤوس HTTP الأمنية
app.use(mongoSanitize());             // منع حقن NoSQL
app.use(xss());                       // تنظيف XSS
app.use(hpp());                       // منع تلوث المعاملات

// CORS محدد
const corsOptions = {
  origin: ["https://myapp.com", "https://admin.myapp.com"],
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true
};
app.use(cors(corsOptions));

// تحديد حجم الطلب
app.use(express.json({ limit: "10kb" }));

اختبار API مع Postman

Postman هو الأداة الأكثر شعبية لاختبار واجهات برمجة التطبيقات. يوفر واجهة رسومية سهلة لإرسال طلبات HTTP بجميع أنواعها وفحص الاستجابات وإنشاء مجموعات اختبار قابلة لإعادة الاستخدام. يمكنك إعداد بيئات مختلفة للتطوير والإنتاج مع متغيرات مشتركة مثل عنوان API ورمز المصادقة:

  • إنشاء مجموعة (Collection): جمّع جميع طلبات API في مجموعة منظمة مع مجلدات لكل مورد (Users, Products, Orders) لسهولة الإدارة والمشاركة مع الفريق
  • البيئات (Environments): أنشئ بيئات مختلفة مثل Development و Staging و Production مع متغيرات مثل base_url و token لتبديل سريع بينها
  • الاختبارات التلقائية: اكتب scripts اختبار بعد كل طلب للتحقق من كود الحالة وبنية الاستجابة ووقت الاستجابة تلقائياً
  • تشغيل المجموعة: استخدم Collection Runner لتشغيل جميع الاختبارات دفعة واحدة والحصول على تقرير شامل بالنتائج

ملخص المهارات المكتسبة

  • بناء خادم Express مع هيكل مشروع منظم
  • إنشاء نماذج بيانات مع Mongoose والتحقق من البيانات
  • تنفيذ عمليات CRUD كاملة (إنشاء، قراءة، تحديث، حذف)
  • إضافة فلترة، ترتيب، وتصفح الصفحات (pagination)
  • معالجة الأخطاء بشكل احترافي
  • أساسيات التحقق من الهوية باستخدام JWT

الخطوة التالية: أضف ميزات مثل رفع الصور، إرسال بريد إلكتروني، والتخزين المؤقت (caching) لتحسين الأداء.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 350.000 FCFA
Parlons de Votre Projet
Publicité