تصميم REST API: الدليل الكامل لبناء واجهات برمجية تدوم طويلًا

17, 2026 · 18 دقيقة قراءة
غلاف المقال بعنوان REST API Design مع رسم لخادم API وأيقونات السحابة والحماية والتحليلات
blog

تصميم REST API هو أمر سهل. الجزء الصعب حقًا هو تصميم واجهة لن تكرهها بصمت بعد مرور عام من الآن.

في اللحظة التي يكتب فيها مطور آخر كودًا يتكامل مع الـ endpoints الخاصة بك، فإنك تكون قد قطعت له وعدًا: هذا الـ URL سيعمل دومًا، وهذا الحقل يُدعى price، وهذا الـ call سيعيد الرمز 201. إذا غيرت رأيك لاحقًا، فلن يقتصر الأمر على عمل refactoring للكود الخاص بك فحسب، بل ستقوم بكسر الكود الخاص بهم، وفي بيئة الإنتاج (production)، وغالبًا يوم الجمعة!

لهذا السبب يحتوي تصميم الـ APIs على الكثير من “القواعد”. إنها ليست مجرد بيروقراطية؛ بل هي الندوب المتراكمة لأشخاص حشروا أنفسهم في زوايا ضيقة كان الخروج منها مكلفًا للغاية. الخبر السار هو أن معظم هذه القرارات لها إجابة صحيحة واضحة، وقد قمت بجمعها هنا في مكان واحد. اقرأ الدليل من البداية إلى النهاية، أو انتقل مباشرة إلى القسم الذي تحتاجه وعد لاحقًا.

الهدف الكامل هو بناء API قابل للتوقع (predictable). يجب أن يكون المطور الذي لم يرَ وثائقك (docs) من قبل قادرًا على تخمين كيفية عمل الـ API وتصيب توقعاته في معظم الأوقات.


ما تطلبه منك فلسفة REST فعليًا

القيود الأساسية التي تطلبها فلسفة REST من الـ API

إن REST هو نمط معماري (architectural style) وليس مكتبة تقوم بتثبيتها. هناك بعض القيود (constraints) التي تهمك في عملك اليومي:

يظل الـ client والـ server مستقلين تمامًا. لا يهتم الـ front end بكيفية تشكيل قاعدة البيانات (database)، ولا يهتم الـ server بما إذا كان المتصل متصفحًا، أو تطبيق هاتف، أو cron job.

كل طلب (request) يقف بذاته (stateless). لا يحتفظ الـ server بأي ذاكرة عنك بين المكالمات، لذا يحمل كل request كل ما يحتاجه ليتم فهمه وتلبيته. قد يبدو هذا كأنه قيد يعيقك، حتى تحاول تشغيل عشر نسخ من خدمتك خلف load balancer، وعندها ستدرك أنه الشيء الوحيد الذي ينقذك.

توضح الاستجابات (responses) ما إذا كان يمكن عمل caching لها، وتعيش الـ resources خلف مجموعة صغيرة ومعيارية من الـ HTTP methods. هذا القيد الأخير هو تحديدًا ما يكسره المطورون باستمرار، ومعظم هذا الدليل يدور حول احترامه.

لا تحتاج إلى التعريفات الأكاديمية الجافة. أنت بحاجة إلى تطبيق هذه الأفكار عمليًا، ويبدأ ذلك من التسمية (naming).


تسمية الـ Resources وتصميم الـ URLs

تسمية الـ resources وتصميم الـ URLs في REST APIs

الـ resource هو “الشيء” الذي تعرضه الـ API الخاصة بك: مستخدم (user)، طلب (order)، أو منتج (product). الـ URLs تقوم بتسمية هذه الأشياء. لذا، قم ببنائها باستخدام الأسماء (nouns)، ودع الـ HTTP method توفر الفعل (verb).

استخدم الأسماء (Nouns) وليس الأفعال (Verbs)

عندما تضع الإجراء (action) داخل المسار (path)، سينتهي بك الأمر بتكرار الفعل الذي يوفره لك بروتوكول HTTP بالفعل، ثم الغرق في مئات الـ endpoints المخصصة لحالات فردية.

❌ GET  /getAllProducts
❌ POST /createProduct
❌ POST /products/5112/delete

✅ GET    /products
✅ POST   /products
✅ DELETE /products/5112

استخدم صيغة الجمع لمجموعات البيانات (Collections)

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

GET    /users        # everyone
GET    /users/42     # one person
POST   /users        # add someone

لا تفعل هذا: إذا قمت بتسمية القائمة بـ /users والـ record الواحد بـ /user/42، فقد أجبرت كل client على تذكر أي الـ endpoints تعتبر استثناءً.

اجعل العلاقات متداخلة (Nested)، ولكن ليس بعمق

أظهر الملكية عبر التداخل. طلبات العميل تقع تحت هذا العميل:

GET /customers/42/orders                # customer's orders
GET /customers/42/orders/18             # one of them
GET /customers/42/orders/18/items/3     # one of them

مستويان أو ثلاثة مستويات من التداخل كافية تمامًا. في اللحظة التي تجد فيها نفسك تكتب URLs مثل هذه، توقف فورًا:

  • /customers/42/orders/18/items/3/supplier/9 أو
  • /companies/12/warehouses/4/aisles/2/shelves/45/products/908 مثل هذه الـ URLs تكون بائسة عند بنائها وتنكسر بمجرد أن يتحرك أي شيء. بدلًا من ذلك، ارفع الشيء ليكون resource رئيسيًا في المستوى الأعلى واستخدم الـ filtering.

حافظ على بساطة وهدوء المسارات (Paths)

اجعل كل شيء بأحرف صغيرة (lowercase)، واستخدم الشرطات (hyphens) للأسماء متعددة الكلمات: /shipping-addresses، وليس /shippingAddresses أو /Shipping_Addresses. تجنب الشرطات المائلة في نهاية المسار (trailing slashes). وتجنب امتدادات الملفات مثل .json (نوع المحتوى يحدد في الـ header وليس في الـ URL). واستقر على أسلوب تسمية واحد لحقول الـ JSON الخاصة بك، سواء كان snake_case أو camelCase، ولا تخلط بينهما أبدًا.

أعلم أن نصيحة “كن متسقًا” تبدو بديهية أو مكررة، لكنها ليست كذلك. الاتساق (consistency) هو الميزة التي تتيح للمطور تخمين الـ endpoint التالي بشكل صحيح بدلاً من فتح مستنداتك (docs) مع كل call.


الـ HTTP Methods: دع الفعل يحمل المعنى

الـ HTTP methods والمعنى الذي يحمله كل فعل

الـ method تحدد ما تقوم بفعله. اضبط هذا الأمر بشكل صحيح لتظل الـ URLs الخاصة بك مستقرة بينما يظل السلوك واضحًا.

Methodالغرض منهاSafe؟Idempotent؟
GETقراءة شيء ماYesYes
POSTإنشاء شيء ما، أو بدء إجراء لا يمكنك تكراره بأمانNoNo
PUTاستبدال الـ resource بالكاملNoYes
PATCHتعديل جزء من الـ resourceNoMaybe
DELETEحذف الـ resourceNoYes
QUERYتنفيذ قراءة معقدة يوضع فيها الاستعلام داخل الـ body، اقرأ المزيد عنها في قسم QUERYYesYes

كلمة “Safe” تعني أن الـ call لا يغير أي حالة على الـ server. وكلمة “Idempotent” تعني أن استدعاء الـ call عشر مرات يترك النظام في نفس الحالة تمامًا كما لو استدعيته مرة واحدة.

هناك أمران يترتبان على ذلك ولا يمكن التهاون فيهما. أولاً، يجب ألا يقوم الـ GET بتغيير الحالة أبدًا. قد يبدو “الـ GET الذي يحذف” غير ضار حتى يقوم browser prefetcher أو crawler بمسح نصف قاعدة البيانات الخاصة بك! ثانيًا، الـ PUT والـ PATCH ليسا متطابقين: أرسل الكائن بالكامل مع PUT، وأرسل فقط الحقول المعدلة مع PATCH.

تحتاج إلى تطبيق مبدأ “Idempotency” مع الـ POST أيضًا، لكنه لا يأتي من مواصفات HTTP نفسها؛ بل هو نمط يمكنك إضافته إلى الـ API الخاصة بك حتى يتمكن الـ clients من إعادة محاولة request ينشئ شيئًا ما بأمان دون إنشائه مرتين عن طريق الخطأ. سنتوسع في ذلك لاحقًا. يمكنك قراءة المزيد عن الـ idempotency هنا


الـ QUERY: عمليات قراءة تخطّت حدود الـ URL

طريقة الـ QUERY تحمل الاستعلام داخل الـ request body بأمان

عاجلًا أم آجلًا، ستنمو في كل API عملية قراءة لا يتسع لها الـ URL. شاشة بحث فيها عشرون filter اختياريًا. أداة لبناء التقارير. أي شيء يكون فيه الاستعلام مستندًا كاملًا، وليس ثلاثة parameters.

حتى وقت قريب، كان أمامك حلان التفافيان فقط، وكلاهما حل وسط على مضض. الأول أن تستمر في حشو الـ query string حتى تصطدم بحد أقصى لم تختره أنت — فالمواصفة لا تُلزم الأنظمة إلا بقبول نحو 8,000 octets، وكثير منها يفرض أقل من ذلك، وكل filter ينتهي به المطاف في الـ server logs وسجل المتصفح والـ bookmarks. والثاني أن تمرر البحث من خلال POST /products/search، وهو حل يعمل لكنه يكذب: فالـ POST ليس safe وليس idempotent، لذا تتجاهل الـ caches الاستجابة، ولا يستطيع الـ client إعادة محاولة request انقطع في منتصفه دون أن يتساءل عما فعله مرتين للتو. أنت تعلم أنها عملية قراءة، لكن HTTP لا يعلم.

لم يضف بروتوكول HTTP أي method جديدة للأغراض العامة منذ PATCH في عام 2010، وفي يونيو 2026 فعلها أخيرًا. جاء RFC 10008 — الذي استغرق إعداده سنوات، وكان يسمى SEARCH في مسوداته الأولى — ليعرّف QUERY: طريقة safe و idempotent مثل GET، وتحمل body مثل POST.

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
 
{
  "category": "books",
  "price": { "min": 10, "max": 50 },
  "in_stock": true,
  "sort": ["-created_at", "name"]
}

تعود النتائج في 200 عادي مثل أي عملية قراءة. لكن لأن الـ method نفسها أصبحت تعلن “هذه عملية قراءة”، صار بإمكان كل ما يقف في المنتصف أن يتعامل معها على هذا الأساس أخيرًا. يستطيع الـ clients والـ proxies إعادة محاولة QUERY بعد فشل الاتصال دون طقوس الـ idempotency-key التي يحتاجها الـ POST. والاستجابات قابلة للـ caching، مع دمج الـ request body داخل مفتاح الـ cache حتى لا يتصادم استعلامان مختلفان أبدًا. والطلبات الشرطية (conditional requests) تعمل تمامًا كما مع GET: أرسل If-None-Match واحصل على 304 رخيصة التكلفة عندما لا يتغير شيء.

هناك بعض القواعد المرافقة. تحديد Content-Type في الطلب إلزامي؛ ويجب على الـ server رفض أي QUERY بدونه بدلًا من تحسس محتوى الـ body والتخمين. والـ status codes تتصرف بالطريقة التي تتمناها: 415 عندما لا تدعم صيغة الاستعلام تلك، و 400 عندما لا يطابق الـ body النوع المعلن عنه، و 422 عندما يُقرأ الاستعلام بنجاح لكنه لا يستطيع العمل فعليًا.

تمنحك المواصفة أيضًا اثنين من الـ response headers يستحقان الانتباه. Location يمكن أن يشير إلى URL يعيد تشغيل نفس الاستعلام كطلب GET عادي، حتى يتمكن الـ clients من تكراره دون إعادة إرسال الـ body. و Content-Location يمكن أن يشير إلى نسخة مخزنة من هذه النتيجة بعينها. كلاهما اختياري، وكلاهما ممتاز للاستعلامات المكلفة.

أما اكتشاف الدعم (discovery) فبسيط بلا مفاجآت: يمكن للـ resource أن يعلن عن دعمه عبر response header باسم Accept-Query يسرد الصيغ التي يقبلها ‏(Accept-Query: application/json)، أو يمكنك البحث عن QUERY داخل الـ Allow header ضمن استجابة OPTIONS، أو ترسل طلبًا ببساطة وتتعامل مع الـ 405.

لا تفعل هذا: لا تلجأ إلى GET مع request body بدلًا من ذلك. المواصفة لا تمنح أي معنى لـ body مرسل مع GET، وأي proxy أو cache أو server على طول المسار حر في إسقاطه. قد يفلت Elasticsearch بهذا الأسلوب داخل cluster تتحكم أنت به؛ أما أي API عام فلن يفلت به.

هل يجب أن تطلق QUERY اليوم؟ افحص الـ stack الخاص بك أولًا. المعيار جديد تمامًا، لذا فإن دعم الـ frameworks والـ gateways والـ CDNs ومكتبات الـ clients لا يزال في طور الاكتمال — كما أن استدعاءات المتصفح تدفع ثمن CORS preflight إضافي، لأن QUERY ليست ضمن القائمة الآمنة (safelist). وحيثما لا تكون أدواتك جاهزة، يبقى POST /search المسمى بوضوح هو الحل البديل الصادق. وبالنسبة للـ filters البسيطة، استمر في فعل ما يقوله قسم الـ filtering: حتى المعيار نفسه يشير إلى أن الاستعلامات القصيرة مكانها الـ query string. أما الـ QUERY فهي للاستعلامات التي تخطّت تلك الحدود.


الـ Status Codes: قل الحقيقة بشأن ما حدث

الـ status codes التي تصف ما حدث فعليًا

أعد الـ code الذي يصف النتيجة الفعلية. تعلم مجموعة صغيرة وشائعة جيدًا بدلاً من البحث عن أكواد غريبة لا يتعرف عليها أحد.

النجاح (2xx)

  • 200 OK — النجاح المعتاد كل يوم.
  • 201 Created — قمت بإنشاء شيء ما. أضف Location header يشير إليه.
  • 202 Accepted — قبلت الطلب ولكنك ستقوم بمعالجته لاحقًا.
  • 204 No Content — نجحت العملية ولكن لا يوجد شيء لإرساله (ممتاز لـ DELETE).

إعادة التوجيه (3xx)

  • 301 Moved Permanently — يعيش هذا العنصر في URL جديد الآن.
  • 304 Not Modified — النسخة المخزنة مؤقتًا (cached) لدى الـ caller لا تزال صالحة (هذا صديقك؛ سنشرحه أكثر في قسم الـ caching).

أخطاء جهة الـ Client ‏(4xx)

  • 400 Bad Request — الطلب نفسه غير صحيح التنسيق (malformed).
  • 401 Unauthorized — لا تعرف هويتهم بعد.
  • 403 Forbidden — تعرف هويتهم، لكن لا يُسمح لهم بالقيام بهذا الإجراء.
  • 404 Not Found — لا يوجد شيء بهذا الاسم.
  • 405 Method Not Allowed — الـ URL صحيح، لكن الـ verb (الأسلوب) خاطئ.
  • 409 Conflict — يتعارض مع الحالة الحالية، مثل محاولة إنشاء تكرار.
  • 422 Unprocessable Entity — الطلب سليم التنسيق ولكنه فشل في عملية التحقق (validation).
  • 429 Too Many Requests — يرسلون طلبات بمعدل سريع جدًا يتجاوز الحد المسموح.

أخطاء جهة الـ Server ‏(5xx)

  • 500 Internal Server Error — حدث خطأ غير متوقع وانفجر شيء ما في جانبك.
  • 503 Service Unavailable — الخدمة متوقفة أو محملة بشكل زائد حاليًا.

هناك خطيئة واحدة تستحق الذكر صراحة: إعادة الرمز 200 OK مع وجود خطأ مدفون داخل الـ response body. الـ clients يثقون في الـ status line؛ فإذا كذبت هناك، فإن كل معالج أخطاء (error handler) تالٍ سيكذب أيضًا.


الطلبات والاستجابات (Requests and Responses)

أشكال موحدة وقابلة للتوقع للطلبات والاستجابات بتنسيق JSON

استخدم تنسيق JSON. لـ request bodies، وللـ responses، ولكل شيء تقريبًا ما عدا نقل الملفات عبر الشبكة. وقم دائمًا بتعيين الـ header التالي: Content-Type: application/json. إرسال نص بتنسيق JSON بدون هذا الـ header يجبر الـ clients على التخمين والقيام بعملية الـ parse يدويًا.

بعد إجراء POST أو PATCH، قم بإرجاع الـ resource المعدل في الـ response. لقد قام الـ client بتعديله للتو؛ فلا تجبره على إرسال request ثانٍ ليرى كيف أصبح شكله الآن.

حافظ على شكل الاستجابات (responses) متوقعًا وموحدًا. بالنسبة للقوائم (lists)، غلف البيانات وضع معلومات الـ pagination في نفس الكائن حتى تظهر دائمًا في نفس المكان:

{
  "data": [
    { "id": 42, "name": "Wireless Mouse", "price": 24.99 }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 137,
    "next": "/products?page=2&per_page=20"
  }
}

الـ Filtering والـ Sorting والـ Pagination

الـ filtering والـ sorting والـ pagination عبر الـ query parameters

المجموعات (collections) تكبر وتتوسع باستمرار. خطط لذلك من أول commit، وافعل ذلك باستخدام الـ query parameters. لا تقم أبدًا بدمج حد أقصى (limit) ثابت داخل المسار (path).

استخدم الـ Filter لتضييق نطاق البيانات:

GET /products?category=books&status=active&min_price=10

قم بالترتيب (Sort) باستخدام أسلوب مقروء ومألوف. وضع علامة - قبل الحقل للترتيب التنازلي (descending) هو نمط شائع يفهمه الجميع فورًا:

GET /products?sort=-created_at,name

قم بعمل Pagination لأي شيء يمكن أن يعيد قائمة كبيرة. لديك خياران رئيسيان:

الـ Offset-based pagination (أو القائم على رقم الصفحة) هو الأبسط: ?page=3&per_page=20. وهو مثالي لقواعد البيانات الصغيرة والمتوسطة، ولأي واجهة مستخدم (UI) تعرض أرقام الصفحات.

الـ Cursor-based pagination يعيد مؤشرًا مبهمًا (opaque pointer) للدفعة التالية بدلاً من ذلك: ?limit=20&cursor=eyJpZCI6MTQ0fQ. يكلف تطويره جهدًا أكبر، ولكنه يظل صحيحًا وسريعًا حتى عند إدخال صفوف جديدة أثناء تصفح المستخدم، وهذا هو السبب الرئيسي في اعتماد الـ APIs عالية الحركة مثل Stripe عليه. إذا كانت بياناتك تتغير باستمرار أو كانت جداولك ضخمة، فهذا هو خيارك الأنسب.

أياً كان اختيارك، قم بإرجاع بعض البيانات الوصفية (metadata): مثل العدد الإجمالي (total)، أو روابط الـ next والـ previous، حتى لا يترك الـ client حائرًا يتساءل عما إذا كان هناك المزيد من البيانات.

وعندما تكبر الـ filters نفسها على الـ query string، فهذه مهمة لقسم QUERY.


معالجة الأخطاء (Error Handling)

شكل موحد لمعالجة الأخطاء في REST APIs

الأخطاء جزء من واجهتك البرمجية (interface). يجب على الـ clients قراءة الأخطاء برمجياً وليس بالعين المجردة فقط، لذا استخدم شكلاً موحدًا (envelope) في كل مكان. هناك معيار رسمي لذلك وهو RFC 9457 Problem Details, ولكن استخدام نموذج موحد بسيط خاص بك يعتبر كافيًا أيضًا:

{
  "error": {
    "code": "validation_failed",
    "message": "The request contains invalid fields.",
    "details": [
      { "field": "email", "issue": "must be a valid email address" },
      { "field": "age",   "issue": "must be greater than 0" }
    ]
  }
}

الخطأ الجيد يمنح الـ caller رمزًا ثابتًا (stable machine code) ليتفرع الكود بناءً عليه (بحيث يتحقق من validation_failed بدلاً من قراءة وفحص النصوص)، ورسالة واضحة للبشر لقراءة السجلات (logs)، وتفاصيل على مستوى الحقول عند فشل الـ validation. ما لا يجب تضمينه أبدًا في الخطأ هو الـ stack trace، أو جزء من استعلام SQL، أو مسار ملف داخلي؛ فهذه تساعد المخترقين وتربك الجميع.


الـ Versioning

أساليب الـ versioning للواجهات البرمجية

الـ APIs العامة تتغير بمرور الوقت. قم بإصدار نسخ (Version) لـ API الخاصة بك من أول إطلاق حتى تتمكن من التطور والنمو دون كسر التطبيقات التي تعتمد عليك بالفعل.

الأساليب الشائعة للـ versioning:

الـ URI versioning يضع رقم النسخة في المسار مباشرة، مثل /v1/products. وهو الخيار الافتراضي الأكثر شعبية لأنه واضح ومكشوف، ويسهل توجيهه (route) واختباره عبر المتصفح مباشرة دون تعقيد.

الـ Header versioning يحافظ على نظافة الـ URLs عن طريق وضع رقم الإصدار داخل header مخصص مثل Accept: application/vnd.myapi.v1+json. هذا الأسلوب يبدو أكثر ترتيبًا، لكنه أصعب في المعاينة البصرية والـ debugging.

هناك أيضًا أسلوب الترقيم القائم على التاريخ (date-based flavor) الذي تستخدمه بعض الـ APIs الكبيرة، حيث تثبت الإصدار باستخدام شيء مثل X-Api-Version: 2024-03-29. تعمل Stripe و GitHub بهذا الأسلوب، وهو رائع عندما تشحن تعديلات بشكل مستمر.

اختر أسلوبًا واحدًا واستخدمه في كل مكان. القاعدة الذهبية التي تحكم كل هذه الأساليب: إضافة حقل اختياري (optional field) يعتبر تغييرًا آمنًا ولا يتطلب إصدارًا جديدًا، ولكن حذف حقل، أو إعادة تسميته، أو تغيير نوعه يعتبر تغييرًا كاسرًا (breaking change) ويجب أن يوضع في إصدار رئيسي (major version) جديد.


الحماية (Security)

الحماية المطبقة على كل endpoint في الـ API

الأمان والحماية ليس مرحلة تأتي في نهاية المشروع؛ بل هو صفة يجب أن تتوفر في كل endpoint، لذا عامله على هذا الأساس.

قم بتقديم كل شيء عبر بروتوكول HTTPS وارفض بروتوكول HTTP العادي تمامًا. إرسال token عبر HTTP يعني إرساله لأي شخص يتنصت على الشبكة.

بالنسبة للـ authentication، حدد هوية المتصل. الـ API keys بسيطة وممتازة للتواصل بين الخوادم (server-to-server traffic) ولتحديد التطبيق الذي يتحدث إليك. بينما يعتبر OAuth 2.0 و OpenID Connect المعيار المعتمد عندما يمنح مستخدم ما صلاحية الوصول نيابة عنه. الـ JWTs هي tokens ذاتية الاحتواء (self-contained tokens) تُرسل في الـ header وتناسب طبيعة الـ REST عديمة الحالة (stateless) بشكل ممتاز.

الـ authentication هي نصف الحكاية فقط؛ والـ authorization هو النصف الآخر: معرفة من هو هذا الشخص لا تعني أنه مسموح له بالوصول إلى هذا الـ record المحدد. تحقق من الصلاحيات في كل request، واجعل الرفض هو السلوك الافتراضي (default to denying).

قم بفرض حدود على معدل الطلبات (Rate limit) للـ endpoints الخاصة بك حتى لا يتسبب عميل واحد جامح في إسقاط خادمك. عندما يتجاوز أحدهم الحد، أرجع الكود 429 مع الـ header ‏Retry-After حتى تعرف التطبيقات جيدة السلوك متى تتراجع وتهدئ من طلباتها بدلاً من الاستمرار في إرهاق الخادم.

تحقق من صحة (Validate) كل البيانات الواردة. افترض أن كل request معادٍ حتى يثبت العكس. وحافظ على سرية البيانات الحساسة بعيدًا عن الـ URLs تمامًا، لأن الـ tokens الموضوعة في الـ query string ينتهي بها المطاف في الـ server logs وسجل المتصفح وأدوات التحليل (analytics) التي تراقب حركتك. الـ headers وجدت لسبب وجيه!


الأداء والـ Caching

الأداء والـ caching في REST APIs

بعض التحسينات السريعة وغير المكلفة هنا ستقطع بك شوطًا طويلاً:

الـ Cache-Control headers تخبر الـ clients والـ proxies بالمدة التي تظل فيها الاستجابة (response) حديثة وصالحة، مما يجنبك سيلًا من الطلبات عديمة الفائدة لبيانات نادرًا ما تتغير.

تتكامل الـ ETags مع الـ If-None-Match بحيث يمكن للـ client أن يسأل “هل تغير هذا؟” ويحصل على استجابة صغيرة بحجم 304 Not Modified عندما لا يطرأ أي تغيير. هذا يوفر حجم البيانات (bandwidth) المستهلك على كلا الطرفين بجهد شبه معدوم.

قم بتفعيل ميزة الضغط (compression) مثل gzip أو Brotli. فنصوص الـ JSON تتقلص كثيرًا ولا تدفع مقابل ذلك سوى قدر ضئيل من المعالجة.

واسمح للـ clients بطلب بيانات أقل عند الحاجة. استخدام الـ Sparse fieldsets مثل ?fields=id,name,price وميزة الـ opt-in expansion مثل ?expand=author يقلل من حجم البيانات المرسلة وعدد الرحلات للـ server (round trips)، كما يخلصك بصمت من مشكلات استعلامات الـ N+1 query.


الـ Idempotency، لكي لا تتسبب إعادة المحاولة في أي ضرر

أحيانًا تفقد الشبكات الطلبات في منتصف الطريق، وتقوم الـ clients بإعادة المحاولة (retry) عند حدوث ذلك. بالنسبة لأي عملية تنشئ سجلاً أو تنقل أموالاً، فإن تكرار الإرسال (double-submit) يمثل خطأً كارثيًا حقيقيًا وليس افتراضيًا.

الحل يكمن في إرسال header باسم Idempotency-Key. يرسل الـ client مفتاحًا فريدًا مع الـ request؛ وإذا رأى الـ server نفس المفتاح مرتين، فإنه يعيد النتيجة الأصلية المحفوظة بدلاً من تنفيذ العملية مرة أخرى. لقد شاع هذا النمط بفضل Stripe لسبب بديهي: لا أحد يريد سحب الأموال من العميل مرتين لمجرد أن هاتفه تعطل لثانية. اقتبس هذا الأسلوب واستخدمه.


التوثيق (Documentation)

الـ API التي لا يستطيع أحد فهم كيفية عملها هي بمثابة واجهة معطلة. قم بوصف واجهتك باستخدام مواصفات OpenAPI (والتي قد تسمع البعض يسميها Swagger). يتحول هذا الملف المقروء آليًا إلى مصدر الحقيقة الوحيد لديك، ومن خلاله يمكنك توليد توثيق تفاعلي (interactive docs)، ومكتبات برمجية للـ clients ‏(SDKs) بشتى لغات البرمجة، وخوادم وهمية (mock servers) لأغراض الاختبار.

قم بتوثيق كل endpoint، ومعاملاته (parameters)، و — أرجوك — استجابات أخطائه أيضًا، كل ذلك مع أمثلة حقيقية. إن الـ APIs التي تشعر بسلاستها وسهولتها هي تلك التي تجيب مستنداتها (docs) عن سؤالك التالي قبل أن تنتهي حتى من صياغته في ذهنك.


كلمة عن HATEOAS

تتضمن معايير REST الصارمة مفهوم HATEOAS، حيث تحمل الاستجابات روابط تخبر الـ client بالخطوة التالية التي يمكنه اتخاذها، بحيث يتنقل داخل النظام دون الحاجة لكتابة URLs ثابتة (hardcoding) في الكود الخاص به:

{
  "id": 42,
  "status": "pending",
  "_links": {
    "self":   { "href": "/orders/42" },
    "cancel": { "href": "/orders/42/cancel" },
    "items":  { "href": "/orders/42/items" }
  }
}

في العالم الحقيقي، معظم الـ APIs في بيئات العمل الفعلي تطبق هذا المفهوم جزئيًا أو تتجاهله تمامًا، ومع ذلك تظل تعمل وتسمى RESTful طوال اليوم وتؤدي عملها بنجاح. تعامل مع هذا المفهوم كأداة مساعدة وليس كفرض ديني؛ أضف الروابط حيث تفيد المطور فعليًا وتساعده على اكتشاف ما يمكن فعله لاحقًا، ولا تقلق كثيرًا بشأن المثالية والنقاء الأكاديمي.


قائمة التحقق (The Checklist)

قم بتعليق هذه القائمة في مكان تراه باستمرار وألقِ نظرة عليها قبل عملية الإطلاق (shipping):

  • الـ URLs عبارة عن أسماء (nouns)؛ والـ method هي الفعل (verb).
  • المجموعات (collections) تأتي بصيغة الجمع ومسماة بشكل متسق.
  • العلاقات لا تتداخل (nest) لأكثر من مستويين أو ثلاثة مستويات.
  • المسارات مكتوبة بأحرف صغيرة (lowercase) ومفصولة بشرطات هيفن، بدون trailing slash وبدون امتداد .json.
  • استخدام أسلوب تسمية موحد لحقول الـ JSON في كل مكان.
  • توافق الـ methods مع معناها (GET لا يكتب أبدًا؛ PUT يستبدل؛ PATCH يعدل).
  • استخدام status codes صادقة وحقيقية، وتجنب إرسال رمز 200 يلتف حول خطأ.
  • استخدام JSON مع تحديد الـ Content-Type الصحيح.
  • تطبيق الـ filtering، والـ sorting، والـ pagination عبر الـ query params مع توفير الـ metadata اللازمة.
  • القراءات المعقدة تستخدم QUERY حيثما يدعمها الـ stack الخاص بك، ولا تستخدم أبدًا GET مع body.
  • شكل موحد للأخطاء يحتوي على الرموز (codes)، والرسائل المفهومة، وتفاصيل الحقول الفاشلة.
  • استخدام الإصدارات (versioned) من اليوم الأول، وتأجيل التغييرات الكاسرة (breaking changes) لإصدار رئيسي جديد فقط.
  • العمل عبر HTTPS فقط، مع تطبيق auth حقيقي وصلاحيات (authorization) فعلية.
  • وضع حدود لمعدل الطلبات (Rate limiting) مع إعادة رمز 429 والـ header ‏Retry-After.
  • تفعيل الـ caching عبر الـ Cache-Control والـ ETags.
  • تفعيل مفاتيح التكرار (Idempotency keys) على أي عملية إنشاء أو دفع وسحب للأموال.
  • توفير وثائق OpenAPI مصحوبة بأمثلة وحالات الأخطاء.

الأسئلة الشائعة (FAQ)

ما الفرق بين PUT و PATCH؟ الـ PUT يستبدل الـ resource بالكامل، لذا فإن أي حقل لا ترسل قيمته سيتم مسحه أو إعادة تعيينه لقيمته الافتراضية. أما الـ PATCH فيقوم بتعديل الحقول المرسلة فقط. استخدم PUT عندما تريد استبدال الكائن بأكمله، واستخدم PATCH عندما تريد تعديل بضع قيم فقط.

هل أستخدم أسماء مفردة أم جمع للـ resources؟ صيغة الجمع دائمًا وبشكل متسق. استخدام /users للقائمة و /users/42 لعنصر واحد يقرأ بشكل طبيعي ويجنب الـ clients عناء حفظ الاستثناءات.

كيف يجب أن أقوم بعمل version لـ API الخاصة بي؟ ابدأ بعمل versioning من الإصدار الأول. يعتبر الـ URI versioning (مثل /v1/...) هو الأسهل في الملاحظة والتوجيه (route)، على الرغم من أن الـ header-based أو الـ date-based versioning يعملان بشكل جيد أيضًا. أيًا كان اختيارك، استخدمه في كل مكان ولا تقم بزيادة الإصدار إلا عند حدوث تغييرات كاسرة (breaking changes).

أيهما أفضل: Offset أم cursor pagination؟ الـ Offset هو الأبسط ومناسب تمامًا لقواعد البيانات متواضعة الحجم والواجهات التي تستخدم ترقيم الصفحات. بينما يحافظ الـ Cursor pagination على سرعة وصحة البيانات عندما تتغير باستمرار في الوقت الفعلي أو عندما تكون الجداول ضخمة للغاية، وهو السبب في تفضيل الـ APIs الكبيرة له.

هل تظل الـ API الخاصة بي تتبع REST إذا تجاهلت HATEOAS؟ من الناحية النظرية الأكاديمية، يعتبر HATEOAS جزءًا لا يتجزأ من REST، ولكن معظم الـ APIs الحقيقية تتجاوزه أو تطبقه بشكل جزئي وتظل تصنف وتعمل كـ RESTful دون مشاكل. أضف الروابط التشعبية (hypermedia) فقط في الأماكن التي تساعد المطور فعليًا؛ وتجنب القلق بشأن المثالية التامة.


كلمة أخيرة

لا شيء من هذا يتطلب ذكاءً خارقًا أو حيلًا معقدة. فالتصميم الجيد للـ APIs يكمن في الانضباط لتكون مملًا وقابلاً للتوقع في كل الأماكن التي يتوقع منك المطور فيها أن تكون كذلك. اضبط الأساسيات بشكل صحيح، واعتمد الـ versioning من اليوم الأول، ولا تجبر أحدًا على قراءة كود المصدر (source code) الخاص بك لمجرد فهم ما يفعله الـ endpoint.

افعل ذلك، وستستمر واجهتك البرمجية (API) في العمل بسلاسة بينما يعم الصخب والاضطراب كل ما حولها. وهو في الحقيقة أجمل ما يمكن لشخص أن يقوله عن أي برمجية بنيتها.

أحمد كمال عمارة
Authors
الرئيس التنفيذي للتقنية · التقنية المالية وهندسة المنصات
أنا رئيس تنفيذي للتقنية بخبرة تتجاوز 18 عامًا في بناء وتوسيع المؤسسات التقنية عبر قطاعات التقنية المالية المنظَّمة والمدفوعات والتجارة الإلكترونية ومنصات المؤسسات. أعمل اليوم رئيسًا للتقنية في ترميز كابيتال، وهي شركة تقنية مالية سعودية تعمل في التمويل والاستثمار عبر الصكوك الإسلامية، حيث انتقلت بالمنصة من فريق من أربعة أشخاص قبل الإطلاق إلى أكثر من 40 موظفًا وإطلاق مرخَّص من هيئة السوق المالية والبنك المركزي السعودي. أكتب عن هندسة الأنظمة الخلفية وقواعد البيانات والأمن والخدمات المصغّرة والبنية التحتية للتقنية المالية — المشكلات التي أواجهها فعليًا في بيئة الإنتاج.