تصميم واجهات البرمجة وأنماط الخدمات المصغرة

أفضل ممارسات تصميم واجهات RESTful البرمجية

5 دقيقة للقراءة

تظهر أسئلة تصميم واجهات البرمجة في كل مقابلة خدمات خلفية تقريبًا. يريد المحاورون أن يروا أنك تستطيع تصميم واجهات برمجية بديهية ومتسقة وجاهزة للإنتاج. يغطي هذا الدرس الأنماط والمقايضات التي تحتاج لمعرفتها.

اصطلاحات تسمية الموارد

تُنمذج واجهات REST البرمجية الموارد كأسماء وليس أفعال. استخدم أسماء الجمع والمسارات الهرمية للتعبير عن العلاقات.

GET    /users                    # سرد جميع المستخدمين
POST   /users                    # إنشاء مستخدم
GET    /users/{id}               # الحصول على مستخدم محدد
PUT    /users/{id}               # استبدال مستخدم
PATCH  /users/{id}               # تحديث جزئي لمستخدم
DELETE /users/{id}               # حذف مستخدم

GET    /users/{id}/orders        # سرد طلبات مستخدم
POST   /users/{id}/orders        # إنشاء طلب لمستخدم
GET    /users/{id}/orders/{oid}  # الحصول على طلب محدد

القواعد الأساسية:

  • استخدم أسماء الجمع: /users وليس /user، /orders وليس /order
  • استخدم الشرطات للموارد متعددة الكلمات: /payment-methods وليس /paymentMethods
  • استخدم التسلسل الهرمي للعلاقات: /users/{id}/orders وليس /getUserOrders
  • لا تستخدم أفعالًا في عناوين URL أبدًا: /users/{id}/activate مقبول فقط كإجراء تحكم، يُفضل PATCH /users/{id} مع { "status": "active" }

طرق HTTP ورموز الحالة

لكل طريقة HTTP دلالات محددة. إتقان رموز الحالة يدل على الخبرة.

الطريقة الغرض قابلة للتكرار آمنة
GET قراءة مورد نعم نعم
POST إنشاء مورد لا لا
PUT استبدال مورد نعم لا
PATCH تحديث جزئي لا لا
DELETE حذف مورد نعم لا

رموز الحالة التي يجب أن تعرفها

الرمز المعنى متى تستخدمه
200 OK نجاح GET وPUT وPATCH مع جسم استجابة
201 Created تم إنشاء المورد POST — أضف ترويسة Location
204 No Content نجاح بدون جسم DELETE وPUT/PATCH بدون جسم مُرجع
301 Moved Permanently إعادة توجيه دائمة ترحيل إصدار الواجهة البرمجية
304 Not Modified الاستجابة المخزنة صالحة GET مع If-None-Match / ETag
400 Bad Request إدخال غير صالح JSON مشوه، حقول مطلوبة مفقودة
401 Unauthorized غير مصادق رمز مفقود أو غير صالح
403 Forbidden غير مخوّل رمز صالح لكن صلاحيات غير كافية
404 Not Found المورد غير موجود المعرف غير موجود
409 Conflict تعارض في الحالة إنشاء مكرر، عدم تطابق الإصدار
429 Too Many Requests تجاوز حد المعدل أضف ترويسة Retry-After
500 Internal Server Error خطأ في الخادم استثناء غير معالج
503 Service Unavailable انقطاع مؤقت صيانة، تعطل تبعية

نصيحة للمقابلة: معرفة الفرق بين 401 (من أنت؟) و403 (أعرف من أنت لكن لا يمكنك فعل هذا) سؤال مقابلة شائع.

أنماط التصفح بالصفحات

كل نقطة نهاية تسرد قائمة تحتاج تصفحًا بالصفحات. للنهجين الرئيسيين مقايضات مميزة.

التصفح بالإزاحة

GET /users?page=2&limit=20
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total_count": 354,
    "total_pages": 18
  }
}

التصفح بالمؤشر

GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "prev_cursor": "eyJpZCI6MTAxfQ==",
    "has_more": true
  }
}

المقارنة

الجانب التصفح بالإزاحة التصفح بالمؤشر
القفز للصفحة N نعم لا
التناسق مع الإدراج/الحذف لا (الصفوف تتحرك) نعم
الأداء في الإزاحات العميقة ضعيف (OFFSET يمسح) ثابت (WHERE id > cursor)
العدد الإجمالي متاح نعم (لكنه مكلف) ليس بسهولة
تعقيد التنفيذ بسيط متوسط
الأفضل لـ لوحات الإدارة، مجموعات بيانات صغيرة التغذيات، البيانات الفورية، مجموعات كبيرة

إجابة المقابلة: "سأستخدم التصفح بالمؤشر لتغذية وسائل التواصل الاجتماعي لأن المستخدمين يضيفون منشورات جديدة باستمرار. التصفح بالإزاحة سيتسبب في انتقال العناصر بين الصفحات. المؤشر يضمن موضعًا مستقرًا في مجموعة النتائج."

التصفية والترتيب واختيار الحقول

استخدم معلمات الاستعلام لمنح العملاء التحكم في الاستجابات.

GET /users?status=active&role=admin&sort=-created_at,name&fields=id,name,email
  • التصفية: ?status=active&role=admin — مرشحات المساواة كأزواج مفتاح-قيمة
  • الترتيب: ?sort=-created_at,name — البادئة - للترتيب التنازلي، فاصلة للترتيب المتعدد
  • اختيار الحقول: ?fields=id,name,email — تقليل حجم الحمولة، مفيد لعملاء الجوال
// TypeScript: تحليل معلمات الترتيب على الخادم
function parseSortParam(sort: string): Array<{ field: string; order: 'asc' | 'desc' }> {
  return sort.split(',').map(field => ({
    field: field.replace(/^-/, ''),
    order: field.startsWith('-') ? 'desc' : 'asc'
  }));
}

// parseSortParam("-created_at,name")
// => [{ field: "created_at", order: "desc" }, { field: "name", order: "asc" }]

أنماط المصادقة

JWT (رموز JSON للويب)

أكثر أنماط مصادقة الواجهات البرمجية شيوعًا. يستخدم رمز وصول قصير العمر ورمز تحديث طويل العمر.

رمز الوصول:   15 دقيقة مدة صلاحية — يُرسل في ترويسة Authorization
رمز التحديث: 7 أيام مدة صلاحية — يُخزن في كوكي httpOnly، يُستخدم للحصول على رموز وصول جديدة
// بنية JWT: الترويسة.الحمولة.التوقيع
interface JWTPayload {
  sub: string;       // معرف المستخدم
  iat: number;       // وقت الإصدار (epoch)
  exp: number;       // وقت الانتهاء (epoch)
  roles: string[];   // مطالبات التخويل
}

// التحقق من جهة الخادم
import jwt from 'jsonwebtoken';

function authenticate(req: Request): JWTPayload {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new UnauthorizedError('رمز مفقود');

  try {
    return jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
  } catch (err) {
    throw new UnauthorizedError('رمز غير صالح أو منتهي الصلاحية');
  }
}

تدفقات OAuth2

التدفق حالة الاستخدام الأمان
Authorization Code تطبيقات الويب (جهة الخادم) الرمز يُتبادل من جهة الخادم، السر لا يُكشف أبدًا
Authorization Code + PKCE الجوال/SPA مُحقق الرمز يمنع الاعتراض، لا حاجة لسر العميل
Client Credentials خدمة-إلى-خدمة آلة-إلى-آلة، بدون سياق مستخدم

مفاتيح الواجهة البرمجية

الأفضل للتواصل بين الخدمات. مرره عبر ترويسة، وليس في عنوان URL أبدًا.

X-API-Key: sk_live_abc123def456

تحديد المعدل

يحمي تحديد المعدل واجهتك البرمجية من سوء الاستخدام ويضمن الاستخدام العادل. خوارزمية دلو الرموز هي الأكثر سؤالًا عنها في المقابلات.

خوارزمية دلو الرموز

import time

class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity          # أقصى عدد رموز في الدلو
        self.tokens = capacity            # الرموز الحالية
        self.refill_rate = refill_rate    # الرموز المضافة في الثانية
        self.last_refill = time.time()

    def allow_request(self) -> bool:
        self._refill()
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

    def _refill(self):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(
            self.capacity,
            self.tokens + elapsed * self.refill_rate
        )
        self.last_refill = now

# مثال: سعة 100 طلب/دقيقة، يُعاد الملء بمعدل ~1.67 رمز/ثانية
bucket = TokenBucket(capacity=100, refill_rate=100/60)

استراتيجيات تحديد المعدل

الاستراتيجية كيف تعمل المزايا العيوب
دلو الرموز الدلو يحمل رموزًا ويُعاد ملؤه بمعدل ثابت يسمح بالاندفاعات، معدل سلس ذاكرة أكثر قليلاً
سجل النافذة المنزلقة تخزين طابع زمني لكل طلب دقيق جدًا استخدام ذاكرة عالٍ
عداد النافذة المنزلقة عد مرجح عبر النافذة الحالية والسابقة ذاكرة منخفضة، دقيق إلى حد ما تقريبي

أرجع دائمًا ترويسات تحديد المعدل حتى يتمكن العملاء من تنظيم أنفسهم:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1704067200
Retry-After: 30

إصدار الواجهات البرمجية

الاستراتيجية مثال المزايا العيوب
مسار URI /v1/users بسيط، صريح، سهل التوجيه عناوين URL تتغير، صعب الإيقاف التدريجي
ترويسة Accept: application/vnd.api.v1+json عناوين URL نظيفة، مرن مخفي عن المتصفح، أصعب في الاختبار
معلمة استعلام /users?version=1 سهل الإضافة يلوث سلسلة الاستعلام، الاختياري قد يسبب أخطاء

الإجابة الشائعة: "إصدار مسار URI هو الأكثر عملية. إنه صريح وسهل التوجيه عند موازن الحمل وبسيط لفهم العملاء. إصدار الترويسة أنظف نظريًا لكنه يضيف احتكاكًا للمطورين الذين يستخدمون المتصفحات أو curl."

مفاتيح قابلية التكرار

طلبات POST ليست قابلة للتكرار — إعادة محاولة دفعة قد تخصم من العميل مرتين. مفاتيح قابلية التكرار تحل هذه المشكلة.

// العميل يرسل مفتاحًا فريدًا مع الطلب
// POST /payments
// Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

async function processPayment(req: Request) {
  const idempotencyKey = req.headers['idempotency-key'];
  if (!idempotencyKey) {
    return res.status(400).json({ error: 'ترويسة Idempotency-Key مطلوبة' });
  }

  // تحقق هل عالجنا هذا المفتاح سابقًا
  const existing = await redis.get(`idempotency:${idempotencyKey}`);
  if (existing) {
    return res.status(200).json(JSON.parse(existing)); // أرجع الاستجابة المخزنة
  }

  // عالج الدفعة
  const result = await chargeCustomer(req.body);

  // خزّن النتيجة (مدة الصلاحية: 24 ساعة)
  await redis.set(
    `idempotency:${idempotencyKey}`,
    JSON.stringify(result),
    'EX', 86400
  );

  return res.status(201).json(result);
}

كيف يعمل: يولّد العميل UUID ويرسله مع كل طلب POST. يخزن الخادم الاستجابة مفتاحة بذلك الـ UUID. إذا وصل نفس المفتاح مرة أخرى (إعادة محاولة الشبكة، نقر مزدوج من المستخدم)، يرجع الخادم الاستجابة المخزنة بدون إعادة المعالجة.

HATEOAS

الوسائط الفائقة كمحرك لحالة التطبيق يعني أن الاستجابات تتضمن روابط للإجراءات المرتبطة.

{
  "id": "order-123",
  "status": "pending",
  "total": 99.99,
  "_links": {
    "self": { "href": "/orders/order-123" },
    "cancel": { "href": "/orders/order-123/cancel", "method": "POST" },
    "payment": { "href": "/orders/order-123/payment", "method": "POST" }
  }
}

في الممارسة العملية، معظم الواجهات البرمجية لا تنفذ HATEOAS بالكامل. لكن ذكرها في المقابلة يُظهر أنك تفهم مستويات نضج REST (نموذج نضج Richardson).

التالي: سنستكشف gRPC مع Protocol Buffers وGraphQL — البديلين الرئيسيين لـ REST. :::

اختبار

اختبار الوحدة 3: تصميم واجهات البرمجة وأنماط الخدمات المصغرة

خذ الاختبار