تصميم واجهات البرمجة وأنماط الخدمات المصغرة
أفضل ممارسات تصميم واجهات RESTful البرمجية
تظهر أسئلة تصميم واجهات البرمجة في كل مقابلة خدمات خلفية تقريبًا. يريد المحاورون أن يروا أنك تستطيع تصميم واجهات برمجية بديهية ومتسقة وجاهزة للإنتاج. يغطي هذا الدرس الأنماط والمقايضات التي تحتاج لمعرفتها.
اصطلاحات تسمية الموارد
تُنمذج واجهات 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. :::