ITSkillsCenter
تطوير الويب

فهم واجهات برمجة التطبيقات REST API: دليل شامل مع أمثلة عملية

8 min de lecture

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

تُعد واجهات برمجة التطبيقات REST API (Representational State Transfer) من أهم المفاهيم في تطوير الويب الحديث. REST هو نمط معماري يحدد مجموعة من القيود والمبادئ لبناء خدمات ويب قابلة للتوسع وسهلة الصيانة. عندما تستخدم تطبيقاً على هاتفك لعرض الطقس أو تصفح منتجات متجر إلكتروني، فإن التطبيق يتواصل مع خادم بعيد عبر REST API للحصول على البيانات وعرضها لك.

في هذا الدليل الشامل، سنتعلم كل شيء عن REST APIs من المفاهيم الأساسية إلى بناء واجهات برمجة كاملة وآمنة واختبارها ونشرها في بيئة الإنتاج. سنستخدم أمثلة عملية مع Node.js و Express.js كإطار عمل رئيسي مع قواعد بيانات MongoDB.

المبادئ الأساسية لـ REST

يعتمد نمط REST على ستة مبادئ أساسية يجب فهمها قبل بناء أي API:

  • Client-Server (العميل-الخادم): فصل واجهة المستخدم عن تخزين البيانات. هذا يسمح لكل جزء بالتطور بشكل مستقل، فيمكن تغيير واجهة التطبيق دون تعديل الخادم والعكس
  • Stateless (بدون حالة): كل طلب من العميل يجب أن يحتوي على جميع المعلومات اللازمة لمعالجته. الخادم لا يخزّن أي معلومات عن حالة العميل بين الطلبات المتتالية
  • Cacheable (قابل للتخزين المؤقت): يجب أن تحدد الاستجابات ما إذا كان يمكن تخزينها مؤقتاً لتحسين الأداء وتقليل الحمل على الخادم
  • Uniform Interface (واجهة موحدة): استخدام واجهة موحدة ومتسقة للتعامل مع جميع الموارد، مما يبسّط البنية ويحسّن الرؤية
  • Layered System (نظام طبقي): يمكن أن تتكون البنية من عدة طبقات وسيطة (مثل موازنة الحمل والتخزين المؤقت) دون أن يعلم العميل بذلك
  • Code on Demand (اختياري): يمكن للخادم إرسال كود قابل للتنفيذ إلى العميل عند الحاجة مثل JavaScript

طرق HTTP الأساسية (HTTP Methods)

تستخدم REST APIs طرق HTTP القياسية لتحديد نوع العملية المطلوبة على المورد:

  • GET: لجلب البيانات – لا يغيّر حالة الخادم ويمكن تكراره بأمان. مثال: GET /api/users لجلب قائمة المستخدمين
  • POST: لإنشاء مورد جديد – يرسل بيانات في جسم الطلب. مثال: POST /api/users لإنشاء مستخدم جديد
  • PUT: لتحديث مورد كامل – يستبدل المورد الحالي بالبيانات المرسلة. مثال: PUT /api/users/123
  • PATCH: لتحديث جزئي – يعدّل حقولاً محددة فقط من المورد. مثال: PATCH /api/users/123
  • DELETE: لحذف مورد – يزيل المورد المحدد. مثال: DELETE /api/users/123

بناء REST API مع Node.js و Express

إعداد المشروع

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

# تثبيت الحزم الأساسية
npm install express mongoose dotenv cors helmet
npm install -D nodemon

# هيكل المشروع
# api-project/
# ├── src/
# │   ├── config/
# │   │   └── database.js
# │   ├── controllers/
# │   │   └── userController.js
# │   ├── middleware/
# │   │   ├── auth.js
# │   │   └── errorHandler.js
# │   ├── models/
# │   │   └── User.js
# │   ├── routes/
# │   │   └── userRoutes.js
# │   └── app.js
# ├── .env
# └── server.js

إعداد الخادم الأساسي

// server.js
const app = require("./src/app");
const mongoose = require("mongoose");
require("dotenv").config();

const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI;

mongoose.connect(MONGO_URI)
  .then(() => {
    console.log("Connected to MongoDB");
    app.listen(PORT, () => {
      console.log("Server running on port " + PORT);
    });
  })
  .catch(err => console.error("DB Error:", err));

// src/app.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const userRoutes = require("./routes/userRoutes");
const errorHandler = require("./middleware/errorHandler");

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true }));

// Routes
app.use("/api/users", userRoutes);

// Error handling
app.use(errorHandler);

module.exports = app;

تعريف نموذج البيانات (Model)

// src/models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, "الاسم مطلوب"],
    trim: true,
    minlength: [2, "الاسم قصير جداً"],
    maxlength: [50, "الاسم طويل جداً"]
  },
  email: {
    type: String,
    required: [true, "البريد الإلكتروني مطلوب"],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, "بريد إلكتروني غير صالح"]
  },
  password: {
    type: String,
    required: [true, "كلمة المرور مطلوبة"],
    minlength: [8, "كلمة المرور قصيرة"]
  },
  role: {
    type: String,
    enum: ["user", "admin"],
    default: "user"
  },
  isActive: { type: Boolean, default: true }
}, { timestamps: true });

userSchema.pre("save", async function(next) {
  if (!this.isModified("password")) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

module.exports = mongoose.model("User", userSchema);

بناء المتحكمات (Controllers)

// src/controllers/userController.js
const User = require("../models/User");

// GET /api/users
exports.getAllUsers = async (req, res, next) => {
  try {
    const { page = 1, limit = 10, sort = "-createdAt" } = req.query;
    const users = await User.find({ isActive: true })
      .select("-password")
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(parseInt(limit));
    const total = await User.countDocuments({ isActive: true });
    res.json({
      status: "success",
      results: users.length,
      totalPages: Math.ceil(total / limit),
      currentPage: parseInt(page),
      data: users
    });
  } catch (err) { next(err); }
};

// GET /api/users/:id
exports.getUser = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id).select("-password");
    if (!user) return res.status(404).json({
      status: "fail", message: "المستخدم غير موجود"
    });
    res.json({ status: "success", data: user });
  } catch (err) { next(err); }
};

// POST /api/users
exports.createUser = async (req, res, next) => {
  try {
    const { name, email, password } = req.body;
    const user = await User.create({ name, email, password });
    user.password = undefined;
    res.status(201).json({ status: "success", data: user });
  } catch (err) { next(err); }
};

// PATCH /api/users/:id
exports.updateUser = async (req, res, next) => {
  try {
    const updates = { ...req.body };
    delete updates.password;
    const user = await User.findByIdAndUpdate(
      req.params.id, updates,
      { new: true, runValidators: true }
    ).select("-password");
    if (!user) return res.status(404).json({
      status: "fail", message: "المستخدم غير موجود"
    });
    res.json({ status: "success", data: user });
  } catch (err) { next(err); }
};

// DELETE /api/users/:id (Soft delete)
exports.deleteUser = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id, { isActive: false }, { new: true }
    );
    if (!user) return res.status(404).json({
      status: "fail", message: "المستخدم غير موجود"
    });
    res.status(204).send();
  } catch (err) { next(err); }
};

تعريف المسارات (Routes)

// src/routes/userRoutes.js
const express = require("express");
const router = express.Router();
const {
  getAllUsers, getUser, createUser,
  updateUser, deleteUser
} = require("../controllers/userController");
const { protect, restrictTo } = require("../middleware/auth");

router.route("/")
  .get(protect, getAllUsers)
  .post(createUser);

router.route("/:id")
  .get(protect, getUser)
  .patch(protect, updateUser)
  .delete(protect, restrictTo("admin"), deleteUser);

module.exports = router;

المصادقة والأمان مع JWT

// src/middleware/auth.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");

exports.protect = async (req, res, next) => {
  try {
    let token;
    if (req.headers.authorization &&
        req.headers.authorization.startsWith("Bearer")) {
      token = req.headers.authorization.split(" ")[1];
    }
    if (!token) return res.status(401).json({
      status: "fail", message: "غير مصرح - سجّل الدخول"
    });
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id);
    if (!user) return res.status(401).json({
      status: "fail", message: "المستخدم غير موجود"
    });
    req.user = user;
    next();
  } catch (err) {
    res.status(401).json({ status: "fail", message: "رمز غير صالح" });
  }
};

exports.restrictTo = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({
      status: "fail", message: "غير مصرح بهذا الإجراء"
    });
  }
  next();
};

معالجة الأخطاء المركزية

// src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
  let statusCode = err.statusCode || 500;
  let message = err.message || "خطأ في الخادم";

  // خطأ MongoDB: معرّف غير صالح
  if (err.name === "CastError") {
    statusCode = 400;
    message = "معرّف غير صالح: " + err.value;
  }

  // خطأ MongoDB: قيمة مكررة
  if (err.code === 11000) {
    statusCode = 400;
    const field = Object.keys(err.keyValue)[0];
    message = field + " مستخدم مسبقاً";
  }

  // خطأ التحقق من صحة البيانات
  if (err.name === "ValidationError") {
    statusCode = 400;
    const errors = Object.values(err.errors).map(e => e.message);
    message = "بيانات غير صالحة: " + errors.join(". ");
  }

  res.status(statusCode).json({
    status: statusCode >= 500 ? "error" : "fail",
    message: message
  });
};

اختبار API باستخدام أدوات مختلفة

اختبار مع cURL

# جلب جميع المستخدمين
curl -X GET http://localhost:3000/api/users \
  -H "Authorization: Bearer YOUR_TOKEN"

# إنشاء مستخدم جديد
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"أحمد","email":"ahmed@test.com","password":"Pass1234"}'

# تحديث مستخدم
curl -X PATCH http://localhost:3000/api/users/USER_ID \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name":"أحمد محمد"}'

# حذف مستخدم
curl -X DELETE http://localhost:3000/api/users/USER_ID \
  -H "Authorization: Bearer YOUR_TOKEN"

الاختبار الآلي مع Jest و Supertest

// tests/user.test.js
const request = require("supertest");
const app = require("../src/app");

describe("User API", () => {
  let token;

  test("POST /api/users - creates user", async () => {
    const res = await request(app)
      .post("/api/users")
      .send({ name: "تست", email: "test@test.com", password: "Test1234" });
    expect(res.status).toBe(201);
    expect(res.body.data.name).toBe("تست");
  });

  test("GET /api/users - returns users list", async () => {
    const res = await request(app)
      .get("/api/users")
      .set("Authorization", "Bearer " + token);
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body.data)).toBeTruthy();
  });

  test("GET /api/users/:id - returns 404", async () => {
    const res = await request(app)
      .get("/api/users/invalidid")
      .set("Authorization", "Bearer " + token);
    expect(res.status).toBe(400);
  });
});

أكواد الحالة HTTP المهمة

  • 200 OK: الطلب نجح – تُستخدم مع GET و PATCH عند نجاح العملية
  • 201 Created: تم إنشاء المورد بنجاح – تُستخدم مع POST عند إنشاء عنصر جديد
  • 204 No Content: نجح الطلب بدون محتوى في الاستجابة – تُستخدم مع DELETE
  • 400 Bad Request: خطأ في بيانات الطلب – بيانات ناقصة أو غير صالحة
  • 401 Unauthorized: غير مصادق – يحتاج تسجيل دخول أو رمز صالح
  • 403 Forbidden: غير مصرّح – مصادق لكن ليس لديه صلاحية
  • 404 Not Found: المورد غير موجود – المسار أو المعرّف خاطئ
  • 422 Unprocessable: البيانات صحيحة الشكل لكن غير قابلة للمعالجة
  • 429 Too Many Requests: تجاوز حد الطلبات – تحديد المعدل فعّال
  • 500 Internal Error: خطأ في الخادم – مشكلة غير متوقعة

أفضل الممارسات لتصميم REST API

  • استخدم أسماء جمع للموارد: /api/users وليس /api/user، و /api/products وليس /api/product
  • هيكل URL متداخل للعلاقات: /api/users/123/orders لجلب طلبات مستخدم محدد
  • تصفية وترتيب وتقسيم الصفحات: استخدم query parameters مثل ?page=2&limit=10&sort=-createdAt&status=active
  • إصدارات API: استخدم /api/v1/users و /api/v2/users للحفاظ على التوافق
  • استجابات متسقة: حافظ على شكل موحد للاستجابات يتضمن status و data و message
  • التوثيق الشامل: استخدم Swagger/OpenAPI لتوثيق كل endpoint مع أمثلة
  • تحديد المعدل (Rate Limiting): حدد عدد الطلبات المسموحة لمنع إساءة الاستخدام

تحسين الأداء وقابلية التوسع

التخزين المؤقت (Caching)

التخزين المؤقت هو أحد أهم تقنيات تحسين أداء REST API. يقلل من الحمل على قاعدة البيانات ويسرّع استجابة الخادم بشكل كبير. هناك عدة مستويات للتخزين المؤقت يمكن تطبيقها:

// استخدام Redis للتخزين المؤقت
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL);

// Middleware للتخزين المؤقت
const cacheMiddleware = (duration) => async (req, res, next) => {
  const key = "cache:" + req.originalUrl;
  try {
    const cached = await redis.get(key);
    if (cached) {
      return res.json(JSON.parse(cached));
    }
    // تعديل res.json لتخزين النتيجة
    const originalJson = res.json.bind(res);
    res.json = (data) => {
      redis.setex(key, duration, JSON.stringify(data));
      return originalJson(data);
    };
    next();
  } catch (err) { next(); }
};

// استخدام الـ middleware
router.get("/", cacheMiddleware(300), getAllUsers);

// حذف الكاش عند التحديث
exports.updateUser = async (req, res, next) => {
  // ... تحديث المستخدم ...
  await redis.del("cache:/api/users");
  await redis.del("cache:/api/users/" + req.params.id);
  // ... إرسال الاستجابة ...
};

تحديد المعدل (Rate Limiting)

تحديد المعدل يحمي API من الاستخدام المفرط وهجمات DDoS عن طريق تقييد عدد الطلبات المسموحة لكل عميل خلال فترة زمنية محددة:

// تثبيت الحزمة
// npm install express-rate-limit

const rateLimit = require("express-rate-limit");

// تحديد عام: 100 طلب كل 15 دقيقة
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: {
    status: "fail",
    message: "تجاوزت الحد المسموح. حاول بعد 15 دقيقة"
  }
});

// تحديد صارم لمسارات المصادقة
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5,
  message: {
    status: "fail",
    message: "محاولات تسجيل دخول كثيرة. حاول بعد ساعة"
  }
});

app.use("/api/", generalLimiter);
app.use("/api/auth/login", authLimiter);

التقسيم إلى صفحات والتصفية والبحث

// نظام متقدم للتصفية والترتيب والتقسيم
exports.getAllProducts = async (req, res, next) => {
  try {
    // بناء الاستعلام
    let query = {};

    // التصفية
    if (req.query.category) query.category = req.query.category;
    if (req.query.minPrice) query.price = { ...query.price, $gte: +req.query.minPrice };
    if (req.query.maxPrice) query.price = { ...query.price, $lte: +req.query.maxPrice };

    // البحث النصي
    if (req.query.search) {
      query.$text = { $search: req.query.search };
    }

    // الترتيب
    const sort = req.query.sort || "-createdAt";

    // التقسيم إلى صفحات
    const page = parseInt(req.query.page) || 1;
    const limit = Math.min(parseInt(req.query.limit) || 10, 100);
    const skip = (page - 1) * limit;

    // تنفيذ الاستعلام
    const [products, total] = await Promise.all([
      Product.find(query).sort(sort).skip(skip).limit(limit),
      Product.countDocuments(query)
    ]);

    res.json({
      status: "success",
      results: products.length,
      pagination: {
        currentPage: page,
        totalPages: Math.ceil(total / limit),
        totalResults: total,
        hasNext: page < Math.ceil(total / limit),
        hasPrev: page > 1
      },
      data: products
    });
  } catch (err) { next(err); }
};

توثيق API باستخدام Swagger

التوثيق الجيد ضروري لأي API ناجح. يوفر Swagger/OpenAPI معياراً لتوثيق APIs مع واجهة تفاعلية للاختبار:

// تثبيت الحزم
// npm install swagger-ui-express swagger-jsdoc

const swaggerUi = require("swagger-ui-express");
const swaggerJsdoc = require("swagger-jsdoc");

const options = {
  definition: {
    openapi: "3.0.0",
    info: {
      title: "User Management API",
      version: "1.0.0",
      description: "واجهة برمجة لإدارة المستخدمين"
    },
    servers: [{ url: "http://localhost:3000" }]
  },
  apis: ["./src/routes/*.js"]
};

const specs = swaggerJsdoc(options);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));

نشر API في بيئة الإنتاج

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

# تثبيت PM2 عالمياً
npm install -g pm2

# تشغيل التطبيق مع PM2
pm2 start server.js --name "my-api"

# عرض حالة التطبيقات
pm2 status
pm2 logs my-api

# إعادة التشغيل التلقائي عند التحديث
pm2 restart my-api --update-env

# حفظ الإعدادات للتشغيل التلقائي
pm2 save
pm2 startup

# ملف ecosystem.config.js
module.exports = {
  apps: [{
    name: "my-api",
    script: "server.js",
    instances: "max",
    exec_mode: "cluster",
    env_production: {
      NODE_ENV: "production",
      PORT: 3000
    }
  }]
};

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

  • فهم مبادئ REST الستة وتطبيقها عملياً في تصميم APIs
  • بناء REST API كامل باستخدام Node.js و Express و MongoDB
  • تنظيم المشروع بنمط MVC مع فصل المسؤوليات
  • تنفيذ عمليات CRUD كاملة مع التحقق من صحة البيانات
  • تأمين API باستخدام JWT والتحكم في الصلاحيات
  • بناء نظام معالجة أخطاء مركزي وشامل
  • اختبار API باستخدام cURL و Postman و Jest/Supertest
  • تطبيق أفضل ممارسات تصميم REST API

الخطوة التالية

بعد إتقان بناء REST APIs، انتقل إلى تعلم GraphQL كبديل مرن لـ REST، واستكشف WebSockets للاتصال ثنائي الاتجاه في الوقت الفعلي. تعلم Docker لتحزيم API الخاص بك و Kubernetes لنشره في بيئات الإنتاج. استخدم Redis للتخزين المؤقت وتحسين أداء API مع التحميل العالي.

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é