بناء RESTful API موافق للمبادئ

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 35 دقيقة (بمعدل فنجانين من الشاي وراحة ما بينهما 😁)

المقدمة

في مقالة كيفية التعامل مع الـ API قمنا باعطاء نبذة شاملة لكيفية التعامل مع RESTful API من وجه نظر الـ Frontend
أما في هذه المقالة سأقوم بشرح كيف تقوم أن كـ Backend محترم ببناء RESTful API محترم
وكيف يمكنه ان يصمم API يقوم باتباع قواعد ومبادئ الـ RESTful API على قد المستطاع

ستجد العديد من العوامل المشتركة بين المقالتين وقد تجد اننا نعيد شرح بعض الاجزاء ذاتها لكن بتفاصيل وزاوية أخرى هذا بالطبع لانهما يتحدثان عن الـ RESTful API لكن من زاويتين مختلفتين، زاوية الـ Frontend في المقالة السابقة وكيفية تعامله مع الـ RESTful API وزاوية الـ Backend في هذه المقالة في إنشاءه للـ RESTful API

تذكر

ما هو الـ RESTful API ؟

الـ RESTful API هو أسلوب أو مجموعة من القواعد تم التعارف عليها تهتم بتصميم وتطوير الـ API وفقًا لمبادئ معينة وثابتة
لجعله سهل الاستخدام والفهم وتستطيع اضافة العديد من المميزات بكل سهولة وبسيطة

الكلام قد يبدو سهل وهو كذالك بالفعل لكن قد تجد بعض الاشخاص يعترضون على بعض الأمور ويضيفون أو ينقصون أو يغيرون بعض الاشياء في الـ RESTful API وهذا أمر طبيعي ستجده في كل شيء موجود في هذه المقالة سأتطرق لبعض المبادئ العامة التي ستراها تطبق بشكل عملي في معظم الـ RESTful API

الـ RESTful API مبادئه كثيرة لكن سهلة الفهم والحفظ والتعود ولست مجبرًا على تطبيقه بشكل حرفي لان قد تحدث بعض التغيرات والاستثناءات لبعض الامور بسبب متطلبات المشروع
أنت فقط حاول دائمًا اتباعه على قد المستطاع

تمثيل وإرسال البيانات على هيئة JSON

أول وأهم شيء لدينا هو اختيار شكل أو صيغة ثابتة لتمثيل وارسال البيانات
وهذه الصيغة يجب ان تكون جيدة لتصف البيانات وتمثيلها بشكل قابل لتعامل معها
بمعنى إن طلب منك صديقك الـ Frontend بتحديث بيانات مستخدم معين وانت ترد عليه بـ Response على هيئة رسالة نصية تقول له User Updated Successfully :) أو أن ترجع له رقم مثل 200 فقط لا غير
أنت هكذا لم تفده بشئ وهو لن يستطيع أن يتعامل مع تلك الرسالة فقط
أريدك كشخص يصمم الـ API أن تضع نفسك دائمًا مكان الـ Frontend الذي سيستخدم هذا الـ API

حسنًا تغير معي الآتي قام صديقك الـ Frontend بطلب تحديث بيانات مستخدم معين فانت رددت عليه برسالة User Updated Successfully :)
الآن الـ Frontend اعتمد التعديل وعدل البيانات المتواجدة معه بالبيانات الجديدة التي عدلها المستخدم

تبدو الأمور جميلة حاليًا، لكن ماذا إذا كانت هناك بيانات اخرى للمستخدم تغيرت بشكل تلقائي ؟
بمعنى انه لنفترض ان المستخدم قام بتغير عنوان اقامته، هذا التغير البسيط قد يغير بيانات اخرى للمستخدم بشكل تلقائي كرمز مركز البريد الخاص بمكان الاقامة الجديد على سبيل المثال
فكيف سيعرف الـ Frontend بهذه التغيرات التلقائية التي تحدث ؟ لذا يريدك أن ترسل بيانات المستخدم ليعرف ما البيانات الاخرى التي تغيرت فالرسالة النصية لن تفيده بشيء

والشكل أو الهيكل التي يفضله ويرشحه الـ Restful API هو الـ JSON في ارسال البيانات لان الـ JSON أصبح هو النظام السائد والمتعارف عليه بين المواقع والمطورين في تخزين وارسال البيانات وأصبح ينصح بها بشدة في تصميم الـ RESTful API
وهذا بسبب بساطتها وتنظيمها ويسهل وصف وتمثيل البيانات بشكل واضح ويسهل عليك قراءتها والتعامل معها بسلاسة وأيضا أصبحت اللغات والمكاتب تدعمها بشكل كامل

لذا بدلًا من أن ترسل رسالة نصية لا تنفع يمكنك أن ترسل كم هائل من المعلومات والتفاصيل كهيئة JSON

{
  "status": "success",
  "code": 200,
  "message": "User updated successfully",
  "data": {
    "id": 2,
    "name": "Ahmed Tab",
    "email": "[email protected]",
    "age": 23
    // ...etc.
  }
}

من خلال قراءتك له تستطيع ان تفهم محتواه بسهولة
فالبيانات مترتبة ومنظمة في هيئة keys وvalues كل عنصر يصف البيانات أو القيمة التي لديه

كل هذه المعلومات تستطيع استخراجها بسهولة بطريقة منظمة من خلال أي لغة برمجية

بالطبع هناك طرق أخرى لتمثيل البيانات مثل XML لكنه غير مدعوم بشكل واسع في المكاتب المختلفة أو حتى اللغات وأحيانا يضطر المبرمجين لاجاد طرق لتحويل البيانات من XML وغيره لـ JSON بطريقة ما
لذا يفضل التمسك بالـ JSON لانه كما قلنا هو الشكل السائد والمنتشر بين المطورين واصبح هو الشكل الثابت والمتعارف عليه في نقل البيانات

اختيار الـ Methods / Actions الصحيح في التعبير عن معناه

الـ methods أو احيانا تسمى actions هي مجموعة من الوظائف أو الأوامر التي تُمرَر للـ API ليعرف ما هو طلبك، هل تريد إحضار شيء ؟ أو اضافة شيء ؟ أو تعديل شيء ؟ أو حذف شيء ؟
نوع الطلب يحدده الـ method

وانواع الـ method كثيرة وأهم 5 انواع أساسية هم:-

method وظيفته
GET تستخدم لإحضار البيانات فقط
POST تستخدم لاضافة بيانات جديدة
PUT تستخدم للقيام باستبدال بيانات عنصر معين بشكل كامل
PATCH تستخدم للقيام بتعديل بيانات عنصر معين بشكل جزئي
DELETE تستخدم لحذف شيء أو عنصر من قاعدة البيانات

حاول ان تصمم الـ API ليتبع المعنى العام لكل method لأن صديقك الـ Frontend عندما يحاول احضار شيء ما هكذا:

GET /api/users

فهو يتوقع منك أن ترجع له جميع المستخدمين ومن غير المنطقي هنا ان تقوم بتعديل البيانات فجأة برغم من أنه استخدم الـ GET

نفس الأمر مع PATCH على سبيل المثال

PATCH /api/users/25

Body: {
  "name": "Ahmed El-Tabarani",
  }

صديقك الـ Frontend عندما يحاول تعديل بيانات المستخدم رقم 25 فهو سيستخدم الـ PATCH
ويعطيك في الـ body الشيء المراد تعديله
هذا ما يتوقعه وهذا هو المعنى المنطقي للـ PATCH أنها تقوم بالتعديل
لذا من غير المنطقي ان تجعلها تقوم بحذف المستخدم بالكامل أو تقوم بوظيفة مغايرة للوظيفة المتفق أو المتعارف عليها

نفس الأمر ينطبق على باقي الـ methods كـ POST للإضافة و DELETE للحذف وهكذا
كل method يجب ان تقوم وتعبر عن المسمى العام لها

الفرق بين PATCH و PUT

عليك ان تدرك الفرق الجوهري بين PATCH و PUT لان البعض يختلط عليه الأمر ويظنهما نفس الشيء ولا يفرقون بينهما

الفرق بينهما متقارب لكن عليك أن تعرف ما هو المتعارف عليه بين PATCH و PUT

أولًا ببساطة الـ PATCH تعدل العنصر أو مجموعة من العناصر بشكل جزئي
أو حتى ببعض الوظائف والعمليات الصغيرة لتنفيذ غرض معين

أما الـ PUT فهو يستخدم للاستبدال الكلي للعنصر وليس لتعديل جزء منه
بمعنى ان كان لديك عنصر معين به name, email age, address
وأرسلت البيانات الجديدة في الـ body الـ name, email فقط فسيتم مسح جميع مكونات العنصر الموجودة مسبقًا ويستبدلهم بالـ name, email وستجد ان العنصر لم بعد لديه age, address لأنه كما قلنا الـ PUT يقوم باستبدال البيانات الجديدة المرسلة في الـ body بالبيانات القديمة

بمعنى ان الـ PUT لا يقوم بعمل تعديل بشكل حرفي بل يقوم باستبدال كل البيانات القديمة بالجديدة

وان لم يكن هذا العنصر موجود فسيتم إنشاءه في قاعدة البيانات
اي ان PUT يعمل عمل الـ POST في حالة ان العنصر المراد تعديله غير موجود في قاعدة البيانات لذا ستجد انه عندما نستخدم الـ PUT يتم ارسال جميع بيانات ومكونات العنصر
بحيث انه اذا كان العنصر موجودا فسيتم استبدال بياناته وان لم يكن موجودًا فسيتم إنشاءه ببساطة

إرسال Status Code يناسب الحدث أو النتيجة

عندما تدخل إلى https://developer.mozilla.org/en-US/docs/Web/HTTP/Status أو https://www.restapitutorial.com/httpstatuscodes.html ستجد العديد من الحالات المتعارف عليها وكل واحدة تستخدم لغرض معين وتصف حدث معين

لذا عليك أن تحاول على قد المستطاع ان تلتزم بارسال الـ Status Code المناسبة ليصف الحدث المناسب ليست مرغمًا على حفظها كلها بالطبع لكن عليك أن تعرف على الاقل الحالات المشهورة والتي ستستخدمها بكثرة

سأعرض الآن جدول يحتوي على أهم الحالات شائعة الاستخدام

Status Code الاسم الوصف
200 OK تم تنفيذ العملية بنجاح وسيرسل الـ Backend البيانات الناتجة من تنفيذ تلك العملية
201 Created تم إنشاء البيانات المطلوبة بنجاح
202 Accepted تم الاستلام بنجاح وجاري تنفيذ العملية
تستخدم مع العمليات المؤجلة أو التي لم تنتهي بعد وتحتاج وقتًا لتنفيذها
انتبه هنا العملية لم تنفذ بعد وقد يحدث خطأ ما في نهايتها، 202 تخبرك بأنه تم استلام الطلب بنجاح لكن لن تخبرك هل العملية ستنجح ام لا
204 No Content تم تنفيذ العمليات بنجاح ولكن لن يرسل الـ Backend أي بيانات خاصة بتلك العملية (لاحظ الفرق بينها وبين 200 في سيرسل ولن يرسل)
400 Bad Request خطأ من ناحية المستخدم وليس من الـ Server، بمعنى ان الـ Server يتوقع منك ارسال البيانات بشكل معين لكن المستخدم ارساله بشكل اخر
401 Unauthorized المستخدم الذي يحاول الوصول لشيء ما داخل الـ Server ليس مصرح له بأي شكل من الاشكال، بمعنى ان الـ Server لا يعرف أي شيء عن هذا الشخص
403 Forbidden المستخدم الذي يحاول الوصول لشيء ما داخل الـ Server مصرح له بالوصول لبعض الامور داخل الـ Server، لكن محظور عليه بعض الامور الأخرى، بمعنى ان الشخص لديه بعض الصلاحيات داخل الـ Server وإن حاول تجاوزها سيعطيه الـ Server أجمل 403 له لينبه ان هذا الشيء محظور عليه
404 Not Found لم يتم العثور على البيانات المطلوبة
409 Conflict لم تنجح العملية بسبب حصول تعارض أو تناقض في البيانات الجديدة والقديمة لسبب ما وغالبا ما يكون السبب هو أنك تريد اضافة بيانات موجودة بالفعل او تريد تفعيل حساب مفعل مسبقًا وهكذا
410 Gone تم حذف البيانات بشكل دائم، بمعنى ان البيانات التي تحاول الوصول اليها لم تعد موجودة على الـ Server بأي شكل من الاشكال بمعنى أنها كانت موجودة مسبقًا لكن الآن حذفت بشكل دائم دون رجعة
429 Too Many Requests لقد قمت بإرسال الكثير من الطلبات في وقت قصير، فأحيانًا يكون هناك حد معين لعدد المحاولات المسموح بها، لتخفيف الهمل والضغط او تقليل محاولات الاختراق
500 Internal Server Error خطأ مجهول من ناحية الـ Server، بمعنى ان هناك شيء ما الـ Server لن يحسب له اي حسبان بالتالي رمز الحالة الافتراضي لأي شيء خطأ مجهول يكون 500

عليك استخدام كل واحدة في مكانها الصحيح فعلى سبيل المثال عندما يحاول صديقك الـ Frontend احضار بيانات مستخدم معين

ان كان هذا الشخص موجود فأرسل 200 OK ليعلم الـ Frontend ان الشخص موجود والقيمة الراجعة من طلب الـ API ستكون بيانات هذا الشخص

وان كان هذا الشخص غير موجود فأرسل 404 Not Found ليعلم الـ Frontend ان الشخص غير موجود والقيمة الراجعة ستكون بها بيانات عن نوع الخطأ بالتفصيل وغالبا ما تكون رسالة توضح ذلك بأن الشخص غير موجود

استعمال الرمز الصحيح للحدث الصحيح يجعل التعامل مع الـ API الخاص بك سهل وسلس وعدم فعل ذلك سيجعل التعامل مع الـ API صعب وغير مريح، فتخيل أنك ترسل دائما 200 في كل حدث حتى وان كان هناك مشكلة وبيانات غير موجودة
او ترسل رمز 400 مع كل خطأ أو مشكلة تحدث حتى وان كانت البيانات غير موجودة أو الشخص غير مصرح به
فالمفترض انك ترسل 404 ان كانت البيانات غير موجودة و 401 أو 403 إذا كان الشخص غير مصرح به
لذا الصحيح أنك يجب ان ترسل الرمز المناسب للحدث الذي يناسبه

أو عندما يظل يطلب بيانات بشكل ناجح وكل مرة يحصل على 200 ثم فجأه يحصل على 429 To Many Requests هنا سيعرف تلقائيًا أن البيانات صحيحة لكنه تجاوز عدد الطلبات المسموحة له

لمزيد من التوضيع عن الفرق بين 401 و 403 يمكنك قراءة هذه الفضفضة الصغيرة من هنا الفرق بين 401 و403

توحيد مسميات الـ Endpoints

يأتي الـ Restful API ويفرض لك نظام تسمية خاص من أجل توحيد المقامات في جزء التسمية

الـ Endpoints يجب أن تعبر عن أسماء وليس أفعال

من المبادئ التي ينادي بها الـ Restful API هو أن تكون مسميات الـ Endpoints تعبر عن اسماء وليس افعال، بمعني أننا لو أردنا أن نحضر كل المستخدمين فسنجعل الـ Endpoints أو إنشاء مستخدم جديد هكذا:

- Good ✔️, Get All Users (simple and clear)
GET /api/users
POST /api/users

فتلك الطريقة واضحة وبسيط GET -> Users استخدما الـ method الذي يعبر عن احضار البيانات GET ثم قلنا له اسم المورد Resource الذي نريد البيانات منه users وهذا الشكل البسيط GET api/users هو ما يتبناه الـ Restful API لتوحيد المقامات على كل شيء نفس الأمر مع باقي الـ methods الأخرى مثل POST عندما نريد إنشاء مستخدم فقط نغير الـ method من GET إلى POST ليعبر بكل بساطة على اننا نريد إنشاء أو اضافة مستخدم جديد

- Get all users from users resource
GET /api/users

- Add new user to users resource
POST /api/users

- Get all article from articles resource
GET /api/articles

- Update article number 25 from articles resource
PATCH /api/articles/25

...etc.

ولا أريدك أن يصل بك الحال وتقوم بفعل شيء قبيح كهذا:

- Bad ❌, (any one can make its own names)
GET /api/users/get-all-users
GET /api/users/all
GET /api/get-users/all

POST /users/create-new-user
POST /users/add-user
POST /users/addNewUser
POST /user/add
POST /user/createOne

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

إذاً، السلوك الصحيح هو استخدام اسم الـ Resource مع الـ method المناسب الذي يعبر عن الغرض الذي ترغب في تحقيقه
نحن نسمي المستخدمين والمقالات أو اي كيان يخزن في قاعدة البيانات بمصطلح الموارد Resources
وفي الـ Restful API نسميها في صيغة الجمع في الـ Endpoints هكذا /users, /articles, ... إلخ ولا نستخدم صيغة الجمع في كل شيء بالطبع، ان كان لدينا عنصر وحيد نريده مثل عنوان مقالة معينة فلا نجمعها بل نجعلها هكذا ببساطة

يمكنك حتى أن تقراه بشكل واضح ومنطقي فعلى سبيل المثال في Endpoint كـ GET /articles/25/title يمكنك قراءته كالآتي اذهب إلى جميع المقالات ثم أحضر المقالة رقم 25 ثم احضر لي عنوانها

الجدول التالي يوضع المسميات الموحدة التي يريدها الـ Restful API عندما نتعامل مع الـ method المختلفة لتنفيذ العمليات كالاضافة والتعديل والحذف وغيرها

Endpoint الوصف
GET /api/users/ احضار جميع المستخدمين
GET /api/users/25 احضار المستخدم صاحب الـ id رقم 25
POST /api/users/ إنشاء مستخدم جديد
PUT /api/users/25 استبدال بيانات المستخدم صاحب الـ id رقم 25
PATCH /api/users/25 تعديل بيانات المستخدم صاحب الـ id رقم 25
DELETE /api/users/25 حذف المستخدم صاحب الـ id رقم 25

تسمية الــ Resources المتداخلة

المقصد هنا انك إذا اردت ان تحضر لي كل المقالات التابعة للمستخدم صاحب الـ id رقم 25
أو تريد اضافة مقالة جديدة له ؟ أو ... إلخ

Endpoint الوصف
GET /api/users/25/articles احضار جميع المقالات التابعة للمستخدم صاحب الـ id رقم 25
GET /api/users/25/articles/5 احضار المقالة رقم 5 التابعة للمستخدم صاحب الـ id رقم 25
POST /api/users/25/articles إنشاء مقالة جديدة للمستخدم صاحب الـ id رقم 25
PUT /api/users/25/articles/5 استبدال بيانات المقالة رقم 5 التابعة للمستخدم صاحب الـ id رقم 25
PATCH /api/users/25/articles/5 تعديل بيانات المقالة رقم 5 التابعة للمستخدم صاحب الـ id رقم 25
DELETE /api/users/25/articles/5 حذف المقالة رقم 5 التابعة للمستخدم صاحب الـ id رقم 25

فصل الكلمات في الجملة بـ -

عندما يكون لديك جملة أو اسم مكون من مجموعة كلمات، نستخدم الشرطة - لفصل هذه الكلمات ببعضها البعض
هذا يسهم في جعل النصوص أكثر وضوحًا اثناء قراءتها

لاحظ هذا الوضوح في الامثلة التالية

- Clear 👌
GET /api/users/25/devices-tokens
PATCh /api/users/25/notification-settings
GET /api/users/25/articles/top-five-rating

- Unclear 🤔
GET /api/users/25/devicesTokens
PATCH /api/users/25/notificationSettings
GET /api/users/25/articles/topFiveRating

وضع أو عدم وضع / في نهاية الـ Endpoint

مسألة إذا كان يجب أن تحتوي روابط الـ Endpoint على / في نهايتها أم لا هو موضوع ستجد جدلًا كبيرًا فيه،

لان مسألة ان تضع / في نهاية الـ Endpoint أم لا يحدث بعض المشاكل
ففي بعض الاحيان عند وضع / في نهاية الـ Endpoint هذا يجعله Endpoint مختلف تماما وبعض المواقع أو لغات البرمجة أو الانظمة قد تعتقد انه مسار مختلف بمعنى أن /api/users لا يساوي /api/users/ بالنسبة لها

سيجد الـ Frontend نفسه يحصل على Not Found 404 أو Internal Server Error 500 لانه يضع أو لا يضع / في نهاية الـ Endpoint

لكي تتخيل لما وكيف تحدث المشكلة فأنظر للمثال التالي:

// GET /api/users/

if (req.url === '/api/users') {
  res.send(/* users data */);
} else {
  res.send('404 Not Found');
}

لاحظ اننا طلبنا /api/users/ لكن الـ Backend أعطانا 404 Not Found لاننا وضعنا / في نهاية الـ Endpoint

لذا أنت كـ Backend يجب أن تختار إحدى الطرق سواء بوضع / في نهاية الـ Endpoint ام لا
وتلتزم بها وتقوم بتحويل الأشخاص بشكل تلقائيًا للطريقة التي اخترتها بشكل سلس إذا استخدموا الأسلوب الأخر

// GET /api/users/

if (req.url.at(-1) === '/') req.url.pop();

if (req.url === '/api/users') {
  res.send(/* users data */);
}

الأمثلة السابقة للتوضيح لا أكثر

في الغالب ستجد إطار العمل Framework الذي تستخدمه يهتم بهذا الموضوع أو به بعض الاعدادات التي يجب ان تقوم بتفعيلها لذا ابحث عن الأمر واختار ما يناسبك

وضع اصدار للـ API الخاص بك | API Versioning

المقصود بـ API Versioning هي عمل اصدارات للـ API الذي تقوم ببناءه
وهذا سيسهل عليك تنظيم الـ API أو اضافة تغيرات جزرية له بكل ببساطة من غير ان تأثر على المطورين الاخرين الذين يستخدمون الـ API الخاص بك

على سبيل المثال عندما تقوم ببناء Restful API وتراه قد وصل لمرحلة مستقرة وجاهز لتنشره ليستخدمه اناس اخرون تبدأ بتحديده على انه الاصدار الأول بوضع /api/v1 في رابط الـ API

بالتالي عندما تقوم بعمل تغير جزري كبير على الـ API تقوم فقط بتغير الاصدار لـ v2
بالتالي لن تقوم بافساد أي تطبيق يستعمل الـ API لانهم سيكونون يستعملون الاصدار السابق ولن يتأثروا بأي تغير
وعندما يريدون ان يحدثوا الـ API فقط سيقومون بقراءة ومعرفة ما الجديد وعندما يكونون مستعدون للتحديث والتأقلم على التغيرات الجزرية الجديدة سيقومون بتغير الاصدار

رفع الاصدار وتغيره يكون فقط عندما يكون هناك تغيير جزري سواء تغير في شكل الرد أو تعديل في المسميات والعناصر أو حتى حذف بعض الـ Endpoint أو إلى اخره من الاسباب

لكن في حين انك تصلح مشكلة ما أو تضيف شيء جديد لن يأثر باي شكل من الاشكال على من يستخدموا الـ API فلا داعي لتغير الرقم

أحيانا بعض الـ API قد تكون تدعم ثلاث خانات لتعريف الاصدار /api/v1.2.3
واسلوب الخانات الثلاثة اسلوب شائع جدًا لوضع اصدار لأي شيء سواء API أو مكتبة أو مشروع

الخانة الأولى تدل على رقم الاصدار بشكل عام وتتغير عندما يحدث تغير جزري
الخانة الثانية تدل على تغير طفيف لا يأثر على شيء أو اضافة Endpoint جديدة أو عناصر جديدة
الخانة الثالثة تعبر عن اصلاحات لمشاكل وتحسينات

وضع الاصدار للـ API ليس شرطًا أن يكون في رابط الـ API
فأحيانا يفضل البعض وضعه في الـ headers أو في الـ query params

- URL:
  /api/v1/users

- Query Params:
  /api/users?version=1

- Header:
  /api/users
  "headers": {
    "Accept": "version=1.0"
  }

Idempotent Methods

يوجد مفهوم شائع في عالم الـ RESTful API وهو مفهوم الـ idempotent وهو مفهوم رياضي في الاصل يسمى تساوي القوى وهو عندما تقوم بتكرار نفس العملية عدة مرات وتحصل على نفس الناتج في كل مرة 1 * 1 * 1 ... * 1 = 1

فبالتالي الـ idempotent في عالم الـ RESTful API عندما نكرر نفس الطلب للـ API عدة مرات ونحصل في كل مرة على نفس النتيجة وهذا يعني أنه بغض النظر عن عدد مرات تكرار طلب، تظل النتيجة ثابتة

فعلى سبيل المثال عندما يقوم أحدهم بعمل GET /api/users/25 10 مرات فإنه يحصل على نفس النتيجة في كل مرة فهنا يمكننا نقول أن اي عملية GET تكون idempotent لاننا عندما نكرر أي طلب GET مرة واحدة أو عشر مرات، النتيجة دائمًا هي نفسها

ماذا عن باقي الـ methods مثل POST أو DELETE؟ فكر قليلًا قبل ان تكمل
عندما نكرر POST /api/users 10 مرات هل سنحصل على على نفس النتيجة في كل مرة ؟ أو عندما نكرر DELETE /api/users/25 10 مرات هل سنحصل على على نفس النتيجة في كل مرة ؟

method هل هو idempotent؟
GET ✔️
POST
PUT ✔️
PATCH
DELETE ✔️

ما رائك باختبار ذكاء أو تحدي تحدي بسيط وهو محاولة استنتاج كل سبب لكل method من الجدول السابق لما هو idempotent أو لا

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

حسنًا لاحظ التالي GET، PUTو DELETE هم idempotent، بينما POSTو PATCH ليست كذلك
حسنًا لنرجع مفهوم الـ idempotent وهو معناه أن تكرار الطلب لن يؤدي إلى تغيير في حالة الشيء المعين

معرفتنا بأن الـ method المعينة تكون idempotent أم لا تفيدنا باتخاذنا بعض الاحتياطات
فعلى سبيل المثال نعرف الان أن تكرار عمليات مثل POST أو PATCH بنفس البيانات تؤدي الى عواقب وخيمة وتغيرات مختلفة داخل قاعدة البيانات خاصتنا كالتكرار او خطأ في الحسابات او تغيرات غير مقصودة مثل ما وضحنا سابقًا
لذا فنحن يجب أن نتخذ اجراءات معينة لتعامل مع تكرار هذه العملية فعلى سبيل المثال:

  1. عندما ننشيء مستخدم جديد نتأكد انه غير موجود مسبقًا
  2. عندما نضيف منتج جديد نتأكد بأننا لم نضف هذا المنتج مسبقًا
  3. عندما نطبق كوبون خصم على المشتريات نتأكد بأننا لم نطبق هذا الكوبون مسبقًا
  4. عندما ... نتأكد ... لكي لا يحدث تكرار أو لبس أو خطأ ما
  5. ... إلخ

على عكس الـ GET أو DELETE فلا ضرر في التكرار لانهما لن يضر الـ Server بهذا التكرار وليس لهما اي تغيرات جانبية في اي مكان اخر في قاعدة البيانات
فتكرارهما ألف مرة سيحدث نفس الناتج عندما نستدعيهما لأول مرة

توحيد شكل الـ Response

من أهم الأمور التي يجب أن تراعيها وأنت تبني الـ Restful API أن يكون شكل الـ Response الراجع للـ Frontend موحد وثابت ومتناسق

لقد اتفقنا على استخدام الـ JSON لتمثيل البيانات لكن الآن نريد ان نوحد هذا الشكل الذي سيتلقاه الـ Frontend أنت كـ Backend يمكنك ان تبتكر الشكل الذي يناسبك او يناسب المشروع او يمكنك ان تتفق مع صديقك اللدود الـ Frontend على شكل ثابت او حتى ان تستخدم شكل شائع متعارف عليه

لنفترض أنك قررت ان تستخدم الأشكال البسيطة التالية للـ Response في مشروعك:

{
  "status": "success",
  "code": 200,
  "message": "Get one article successfully",
  "data": {
    "id": 1,
    "title": "Article title",
    "description": "Article description",
    "content": "Article content"
  }
}
{
  "status": "success",
  "code": 200,
  "message": "Get All articles successfully",
  "length": 2,
  "data": [
    {
      "id": 1,
      "title": "Article title",
      "description": "Article description",
      "content": "Article content"
    }
    {
      "id": 2,
      "title": "Article title #2",
      "description": "Article description :)",
      "content": "Article content ..."
    }
  ]
}
{
  "status": "error",
  "code": 404,
  "message": "Article not found"
}

الآن سيقوم صديقك اللدود الـ Frontend بتجهيز نفسه وعمل احتياطاته ليستقبل هذه الاشكال بحالاتها المختلفة
في الغالب سيقوم بعض الكلاسات والدوال الذي سيستخدمها لتحويل الناتج الراجع من الـ Response الذي سيتلقاه، ويحوله من الـ json إلى شكل او object يناسب اللغة الذي يستخدمها ليستطيع التعامل معها او يتوقع الناتج بسهولة

الآن ماذا سيحدث اذا قررت انت كـ Backend تغير الشكل قليلًا ؟ أو تغير كلمة message إلى Message تغير بسيط
لكن صديقك اللدود لا يعرف هذا التغير وستجده يصرخ ويدور حول نفسه ولا يعرف السبب
واذا اخبرته وكان التعديل بسيطًا ربما يكون التغير من عنده بسيطًا كذلك ويفضل ان تخبره قبلها وتتفق معه على كل تغير سيحصل حتى وان كان بسيطًا جدًا
لانه ان كان تغيرًا كبيرًا كتغير في شكل وعناصر المقالة أو تغير جزري في الشكل الذي كنتما قد اتفقتما عليه سيكون هذا له عواقب وخيمة لانك ستضطر لتغير ذلك في الـ Frontend سواء في منصات المشروع على الويب والهواتف وقد يكون تغيرًا كبيرًا ويحتاج وقت قد يصل لعدة ايام لتعديله واختبار كل شيء
لذا يجب ان تتفق معه على شكل ثابت وتقلل هذه التغيرات على قد المستطاع
لانك قد تجده في يوم من الأيام يمسك فأسه الحاد وينظر إليك بنظرات غير مريحة

لذا التزم دائمًا بشكل ثابت وموحد لجميع الـ Responses التي سترجعها
وتقلل من التغيرات عليه خصوصًا التغيرات الجزرية لذا قبل تنفيذ أي شيء يجب ان تدرسه وتتناقش مع الجميع للوصول لشكل ثابت يريحكم ويكون سلس ويخدم الافكار او المشروع الذي تعملون عليها
وأيضًا لكي لا تفقد رأسك

لانه كما قلنا يسهل على المطورين في الـ Frontend فهم الـ Response ومعالجته والتعامل معه
ويقلل من الأخطاء والمشكلات في الـ Frontend إلى حد كبير بالتالي عملية التطوير تكون أسرع وسلسة

التعامل مع الأخطاء بشكل صحيح | Error Handling

في عالم البرمجة، لا تسير الأمور دائمًا وفقًا للمطلوب أو المتوقع، لأنه دايمًا ما ستحدث الأخطاء لأسباب مختلفة، مثل إدخال بيانات غير صالحة أو غير مطلوبة أو مشاكل في الـ Server أو أخطاء في الكود نفسه كتبتها أنت لأنك إنسان وقد تخطيء في بعض الأحيان وهذا شائع وطبيعيًا
لذا يُعدّ معالجة الأخطاء أي الـ Error Handling من أهم الخطوات في عملية تطوير أي API

عندما يحدث أي خطأ لى سبيل المثال قام المستخدم بإدخال بيانات خاطئة يجب عليك كـ Backendأن توفر معلومات مفيدة وواضحة لصديقك الـ Frontend أو للمطورين الذين سيستعملون الـ API
لكي يستطيعوا فهم الخطأ أي كان ويتعاملون معه بحسب نوعه ويعرضون رسالة توضحية للمستخدم تشرح له الخطأ ليصححه أو ليتخذ إجراء معين

وعرض معلومات كافية ذات صلة بالخطأ الذي حدث يساعد على فهم الخطأ والتعامل معه بسرعة
ويجب أن ترجع دائمًا ترجع الـ Status Code المناسب الذي يصف كل خطأ ونوعه
ولقد تكلمنا عن هذه الاكواد والى ماذا ترمز في الجدول السابق يمكنك ان ترجع له وتراجعه

وخلاصة الأمر أنك يجب أن ترجع خطأ بـ 404 Not Found عندما تكون فعليًا لم تجد الشيء الذي يبحث عنه المستخدم
و ترسل 400 Bad Request عندما يقوم المستخدم بإدخال بيانات خاطئة
و 401 Unauthorized عندما يحاول شخص غير مصرح له باستخدام الـ API الخاص بنا
و ... إلخ
تلك التفاصيل الصغيرة توضح للـ Frontend نوعية المشكلة الذي يتعامل معها وأيضًا لا أظن ان صديقك سيحب أن يرى 200 OK بأن الدنيا جميلة برغم بأن هناك خطأ ولم يتم تنفيذ العملية، تتذكر الفأس؟

وأيضًا أحيانًا يحدث بعض الأخطاء من المستخدم عندما يدخل بعض البيانات وأحيانا يجب على الـ Backend أن يقدم تفصيلاً بالحقول التي وقعت فيها المشكلة
على سبيل المثال:

{
  "status": "error",
  "code": 400,
  "message": "Bad Request ...",
  "errors": [
    {
      "field": "name",
      "message": "Name is required"
    },
    {
      "field": "password",
      "message": "Password must be at least 8 characters long"
    }
  ]
}

وكما قلنا شكل الـ Response لعرض كيف سيكون الـ json الذي سيستقبله الـ Frontend مسؤوليته تكون عليك أنت كـ Backend أن تتفق معه وتشرح له الشكل كيف سيبدو ويجب أن يكون موحد وثابت والأمور التي ذكرناها سابقًا في توحيد الشكل

تتبع الأخطاء في مكان واحد | Monitoring

هناك شيء يدعى Monitoring وهو أن تراقب ما يحدث ومن الأمور البديهية عندما تصمم الـ API أن تعرف كل شيء يدور فيه وكل صغيرة وكبيرة تحدث عليه
لذا يمكنك نظام معين لكي تراقب وتتبع ما يحصل لذا تقوم بعمل Logger System وهو أنك تقوم بتخزين معلومات كل Request في ملفات داخل الـ Server الخاص بك

بالتالي يمكنك في أي وقت تصفح تلك الملفات لمعرفة كل Request تاريخه وبياناته وهل حدث أي خطأ او مشكلة ويمكنك تتبع المشاكل بسهولة ومتى حدثت ما نوعها
وأنت من تحدد المعلومات والتفاصيل التي تخزنها في الملفات لذا اضمن ان تخزن تفاصيل تساعدك على تتبع ومعرفة المشاكل بسهولة مثل timestamp, method, status code, URL, error type, message, ... etc

وأن من تحدد كيف تقسم وتخزن الملفات وتسميها، يمكنك أن تسميها بحسب تاريخ اليوم ونوع المشكلة
شيء اشبه بهذا

Logs Folder
|
├ errors-2024-03-20.log
├ errors-2024-03-21.log
├ errors-2024-03-22.log
...

محتوى الملف كما قلنا يجب أن يكون به معلومات وتفاصيل كافية لمعرفة المشكلة بشكل لحظي

[Project Development] Error 2024-03-20 17:19:42 [Unauthorized] 401 POST /v2/api/auth/login Message: Email or password not correct
[Project Development] Error 2024-03-20 17:19:56 [Expression]
[Project Development] Error 2024-03-20 17:19:56 [Body] {"password":"123456789","email":"[email protected]"}
[Project Development] Error 2024-03-20 17:19:56  [StackTrace] UnauthorizedException: Email or password not correct
  at callback (Project\file\file\file)
  at Project\file\file\file
  at Project\file\file\file
  at ... etc.

ما الفرق بين Stateless و Stateful

حسنًا لدينا نوعين مختلفين أثناء تعاملنا أو بناء أي API وهو أما أن يكون Stateless أو Stateful

يمكننا القول:

الأمر أشبه بأنك لديك ماكينة قهوة في الشركة تقوم بإعداد القهوة بحسب طلب كل شخص
هل هذه المكنة تتأثر بالطلباتك السابقة ؟ أو تحتفظ بأي طلب سابق
هل في يوم من الأيام عندما تطلب قهوة سادة ستجدها فجأه وضعت بعض السكر بمجرد أنك في المرة السابقة طلبت قهوة عليها معلقتين سكر ؟
لا، ماكينة القهوة بطبيعتها كل طلبية تكون مستقلة عن جميع طلباتك الأخرى، بمعنى أنها Stateless

يمكننا أن نتخيل الأمر بشكل مبسط هكذا:

function prepareCoffee(req, res) {
  const order = req.body;

  // Check if ingredients are available in the database or not
  const isAvailable = checkIngredientsAvailability(order);

  if (!isAvailable) throw new Error('Ingredients not available');

  // Prepare the coffee
  const coffee = prepare(order);
  res.send(`Coffee prepared: ${coffee}`);
}

لنتخيل أن Request يستدعي تلك الدالة البسيطة
هل ترى أي تداخل قد يحصل بين كل Request أو أي معلومات تم جلبها من Request سابق وبناءًا عليه تم اجراء عملية معينة ؟
لا، فكل ما تراه هو دالة بسيطة تقوم بفحص المكونات ان كانت موجودة في قاعدة البيانات ام لا ثم تجهز لك قهوتك الجميلة
ولا يوجد أي شيء يتعلق بجلب معلومات سابقة من Request سابق أو ما شابه، لذلك تلك الدالة تعد Stateless

الأمر اشبه بأنك تقوم بالاشتراك في نادي لتحقيق الأحلام وهذا النادي يحقق لك 10 أحلام في كل جلسة

وطريقة عمل هذا النادي تكون كالأتي:

بعد تسجيلك في النادي يقوم أعضاء النادي بتعليق صورتك على حائط النادي وكتابة أنك لديك 10 طلبات فقط لمدة أسبوع واحد
لذا أي طلب ستطلبه في النادي سيتم التعرف عليك أولًا من خلال الحائط، هل انت فيه أم لا
ثم سيتم تنفيذ طلبك بتحقيق حلمك الجميل ثم سيتم خصم عدد الطلبات المتاحة لك

هل لاحظت كيف قام النادي بحفظ بيانات الشخص في طلبه الأول على حائط النادي
ثم مع كل طلب يتم النظر إلى الحائط وبناء على المعلومات التي به يتم خصم وعمل عملية ما ؟
بالتالي كل طلبية تعرف ما حصل في الطلبيات السابقة وعدد المحاولات وهل انت سجل سابقًا أم لا فكل الطلبيات تتشارك المعلومات مع بعضها في هذا الحائط النادي وتحدث البيانات الذي فيه وتتخذ اجراء بحسب حالة الحائط

وهذا النوع بحيث أن كل طلب يتشارك المعلومات التي تحصل في كل الطلبات السابقة في إطار جلسة معينة، فهذا ما نسميه الـ Stateful

لنوضح هذا الكلام بكود يحاكي هذه الخطوات لتتضح الأمور بشكل أفضل:

function subscribeToTheDreamComeTrueClub(req, res) {
  // Get the user that wants to subscribe to the club
  const user = getUserDate(req);

  // Create a temporary session for this user
  createSessionForUser(user);

  res.send('You have successfully subscribed to the dream come true club');
}

لاحظ هنا أنه تم إنشاء Session للمستخدم الذي سجل في هذا النادي
وهذه الجلسة تخزن في الذاكرة المؤقتة الخاصة بالـ Server وليس في قاعدة البيانات
بالتالي كل Request يستطيع الوصول لكل الجلسات الموجودة ذاكرة الـ Server بسهولة

function makeADreamComeTrue(req, res) {
  // Get the session of the requested user
  const session = getUserSession(req);

  if (!session) throw new Error('You are not subscribed to the club yet');

  // Check if the session expired or not
  const isSessionExpired = checkIfSessionExpired(session);

  if (isSessionExpired)
    throw new Error(
      'Sorry your session has expired, please subscribe to the club again'
    );

  // Check if the user has remaining requests or not
  const hasRemainingRequests = checkIfUserHasRemainingRequests(session);

  if (!hasRemainingRequests)
    throw new Error(
      'Sorry you have no remaining requests, please subscribe to the club again'
    );

  // Make the dream come true
  makeDreamComeTrue(session);

  // Decrease the remaining requests
  decreaseUserRemainingRequests(session);

  res.send('Your dream has come true!');
}

هنا يتم جلب الـ Session الحالية الموجودة في الذاكرة المؤقتة الخاصة بالـ Server كما قلنا وهذه الجلسة يتم التعديل عليها والبيانات التي فيها من كل Request مر على هذه الجلسة
فكما ترى فنحن نتحقق أولًا هل هناك جلسة مفتوحة لهذا المستخدم في هذا الـ Server أم لا
ثم هل انتهت صلاحيتها أم لا ثم هل نفدت عدد المحالات أم لا
ثم في النهاية يتم تحقيق الحلم وتعديل البيانات الخاصة بالجلسة

يمكننا توضيح الفروق بشكل افضل في هذا الجدول البسيط:

Stateless Stateful
الـ Server لا يحتفظ بأي معلومات من أي Request ولا ينشيء أي Session الـ Server يحتفظ بالمعلومات من كل Request سابق داخل Session
كل Request يحتوي على كافة المعلومات اللازمة لاستكماله بمفرده، دون الحاجة إلى معرفة سابقة عن Request أو Session معين يتم تبادل البيانات بين كل Request ويمكن أن يتأثر تنفيذ Request ما بحالة أو بمعلومات في Request سابق بمعنى أن الـ Server يكون على دراية بالسياق العام لمجموعة من الـ Requests داخل إطار Session معين
طالما أن كل Request منعزل عن الآخر فستقل المشاكل والتعقيد ويسهل توسيعه وعمل أكثر من Server دون الحاجة إلى مزامنة معلومات المستخدم بين كل Request مع الآخر في كل Server بحكم أن الطلبات او العمليات منعزلة ولا تتداخل مع بعضها البعض يصعب تعديله او توسيعه بحكم أن كل Request يتداخل مع الآخر بالتالي قد تحصل مشاكل غير متوقعه ويجب أن يتم مزامنة معلومات المستخدم في كل Session خاصه به بين كل Request مع الآخر في كل Server
يستخدم في التطبيقات التي تتعامل مع كل طلب بشكل منعزل ومنفصل عن باقي الطلبات مثل أغلب التطبيقات اليوم مثل مواقع الشراء الالكترونية او مواقع التواصل الاجتماعي او المواقع الخدماتية بشكل عام يستخدم في الوظائف التي يكون للمستخدمين جلسة معينة لمدة معينة مثل تطبيقات البنكية على سبيل المثال أو عندما تفتح جلسة مع تطبيقات خدمة العملاء وغيرها من تلك الأمور
في مثال ماكينة القهوة يمكننا ان نضع 20 ماكينة في أماكن مختلفة وكل ماكينة منعزلة على الأخرى ولا تتطلب مزامنة الطلبات بينها في مثال نادي تحقيق الاحلام إذا أراد النادي أن يتوسع ويبني فروعًا في عدة أماكن يجب مزامنة المعلومات التي يحصل عليها كل نادي مع كل فرع وكل فرع يجب أن يعرف كل الاعضاء الذي يشتركون بالفروع الأخرى لكي يزامنوا بياناتهم في حالة قرر شخص أن يستكمل تحقيق أحلامه في فرع آخر

ملحوظة: علينا أن نفرق بين الـ Session و Database

في الـ Restful API يفضل استخدام الـ Stateless

ستجد دائًما ما يفضل استخدام الـ Stateless في الـ Restful API والسبب أن كل عملية او طلب تكون منعزلة تمامًا على باقي العمليات الأخرى لانها لا تخزن أو تستخدم مفهوم الـ Session
بالتالي ستقل المشاكل والتعقيد ويسهل توسيعه كما قلنا ويسهل عمل أكثر من Server دون الحاجة إلى أي معلومات في الذاكرة المؤقتة في كل Server

في مثال ماكينة القهوة عندما نفترض أن ماكينة القهوة تلك هي الـ Server الخاص بنا
قد يخطر ببالكم شيء ما ماذا اذا كان لدينا ماكينة قهوة تحضر بعض انواع القهوة الفاخرة لاشخاص معيين في الشركة بحسب صلاحيتهم ومنصبهم
فهكذا تلك الماكينة ستتطلب من المستخدم تسجيل بياناته لكي تستطيع المكنة التفريق بينهم وبين صلاحيتهم
حسنًا بحكم أن المكنة تلك Stateless بالتالي أن ادخل شخص ما بياناته ثم طلب قهوته ففي المرة القادمة عليه أن يدخل بياناته مجددًا
لأن المكنة لا تحتفظ ببياناتك السابقة في الذاكرة المؤقتة الخاصة بها قد يكون المستخدمين مسجلين في قاعدة بيانات بالفعل لكن كل شخص عليه ان يدخل بياناته في كل مرة لتستطيع المكنة التحقق منه

فكيف نحل هذه المشكلة مع الحفاظ على كون المكنة Stateless ؟

يمكننا ان نقوم بعمل نوع من انواع بطاقات التعريف لكل شخص بالتالي عندما يطلب أحد ما قهوة من الماكينة
فيمكنه أن يدخل البطاقة التعرفية الخاص به للمكنة والمكنة ستتعرف على هذا الشخص بناءًا على البيانات المكتوبة في البطاقة

مفهوم البطاقة التعرفية نسميها Token وهي مجرد نص مشفر يحمل بيانات وهذا الـ Token لا يتم تخزينه في الـ Server
بل يكون مع المستخدم يرسله مع كل Request ليثبت أو يعرف هويته للـ Server
الـ Token يقوم صديقك الـ Frontend بتخزينه في الـ LocalStorage ويضعه في الـ headers مع كل Request عندما يرسل أي Request أو يمكننا أن نخزنه في الـ Cookies الأمر عائد لكما كـ Frontend و Backend كيف تديران الأمر

فكر بالـ Token على أنها بطاقة الهوية الخاصة بك التي ستستخدمها للحصول على فنجان القهوة الجميل الخاص بك

هيا لنرى أو نحاكي هذه العملية ككود ونشرحها لتتضح الأمور بشكل أفضل:

function prepareCoffee(req, res) {
  // Check token is valid or not
  const token = checkToken(req);
  if (!token) throw new Error('Invalid token');

  // Get user info
  const user = getUserInfo(token);

  // check if user is exist in database or not
  if (!user) throw new Error('User not found');

  const order = req.body;

  // Check if ingredients are available in the database or not
  const isAvailable = checkIngredientsAvailability(order);

  if (!isAvailable) throw new Error('Ingredients not available');

  // Prepare the coffee for specified user
  const coffee = prepareOrderForUser(order, user);
  res.send(`Coffee prepared: ${coffee}`);
}

لاحظ أن الدالة لم تتغير كثيرا، فقط الشيء الجديد أنها اصبحت تتحقق من صحة الـ Token
وتتعرف على المستخدم هل هو موجود في القاعدة البيانات أم لا
والدالة مازالت Stateless بحيث أن كل Request مازال مستقلًا بذاته
ولا يعتمد على إحضار أي معلومات من أي Request سابق أو من Session معينة

أضف Pagination لتسهل تصفح البيانات وتقليل حجمها

حسنًا عندما تصمم Restful API فمن المهم أن يحتوي على Pagination للبيانات التي لديك
والـ Pagination تعني انك تقسم البيانات خاصتك لاجزاء وتضع فيها شيء كنظام الفهرس لتصفح أي جزء من البيانات بسهولة

تخيل معي أنك لديك بيانات كثيرة على سبيل المثال ألف مستخدم
وكلما أراد الـ Frontend استحضار البيانات يجد نفسه يحصل على الألف مستخدم في كل مرة
فهذا لا يبدو منطقيًا لانه هكذا سيكون حجم البيانات كبير جدًا وغالبا الموقع لن يستطيع التعامل معها

لذا نلجأ لتقييد عدد البيانات الذي نرجعها بمعنى تقليلها بعدد معين
فبدلًا من أن نحصل على ألف مستخدم، نحصل على 10 فقط في كل مرة

ويمكنك جعل صديقك الـ Frontend يقوم بعمل بعض الـ Query ليستطيع التحكم بعدد البيانات التي سترجع له
والـ Query كما تعرف فهو ما نضعه في نهاية أي Endpoint و يكون بعد علامة الاستفهام ?
الأمر يبدو كهذا /endpoint?key=value

وعندما يريد صديقك الـ Frontend بتحديد عدد النتائج الذي يريدها أن ترجع له فيمكنك أن تنشيء متغير لنسميه limit و تجعله يعدله في الـ query
فإن أراد استحضار بيانات أول شخصين فيمكنه أن يقوم الـ query التالي /api/users?limit=2

الآن وإن أراد الحصول على الشخصيين التاليين ؟ كأنك تقول بعد ما قسمت الأشخاص لمجموعات من لشخصين فقط، نريد المجموعة الثانية من هذا التقسيم
هنا يمكنك أن تقوم بعمل متغير آخر لنسميه page وهي بعد ما يتم استخدم limit لتحديد عدد النتاج نريد أن نختار المجموعة التي نريدها يمكنك أن تتخيل أن limit كأنها تقسم الناتج لصفحات و page تختار لك الصفحة التي تريدها

بمعني إن كان لديك 5 أشخاص فقط في قاعدة البيانات
فبعد ما يتم استخدم limit=2 لتقسيم لمجموعات من شخصين بهذا الشكل (1, 2) (3, 4) (5) فهكذا يمكنك أن تتخيل إن قام صديقك الـ Frontend بتحديد page=1 بهذا الشكل /api/users?limit=2&page=1 فيتم استرجاع المجموعة الأولى المكونة من شخصين كما ترى (1, 2)
و /api/users?limit=2&page=1&page=2 هي المجموعة الثانية وهي مكونة من شخصين أيضًا (3, 4)
و /api/users?limit=2&page=1&page=3 هي المجموعة الثالثة والتي بها شخص واحد فقط (5)

بالطبع إن قام الـ Frontend بتجاوز عدد البيانات الموجودة وكتب page=50 فيمكننا أن تعطيه بيانات فارغة

هذا الاسلوب الذي يستخدم limit و page يسمى Page-Based Pagination
ستجد اساليب أخرى لكن الـ Page-Based Pagination أكثرهم بساطة وشيوعًا

من الاساليب الأخرى يمكننا عمل متغير offset وهو تحديد من أين تريد أن تبدأ
بمعنى أنك تقول على سبيل المثال أبدأ استحضر البيانات من عند الرقم 100 وأحضر لي 25 فقط
سيكون شكل الـ query هكذا /api/users?limit=25&offset=100 أحضر 25 مستخدم من بعد المستخدم رقم 100

أضف معلومات الـ Pagination في الـ Response

يفضل دائمًا أن تضع Pagination Metadata في الـ Response لتزويد الـ Frontend بكل المعلومات التي قام بها والتي يستطيع القيام بها في الـ query

فعندما يقوم بعمل التالي /api/users?limit=2&page=3، فيمكنك وضع بعض المعلومات الخاصة بالـ Pagination في الـ Response كالتالي

{
  "status": "success",
  "code": 200,
  "message": "Get users successfully",
  "data": [
    {
      "id": 5,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 6,
      "name": "Osama",
      "age": 30,
      "email": "[email protected]",
      "salary": 5000,
      "country": "Palestine"
    }
  ],
  "pagination": {
    "total": 10,
    "limit": 2,
    "current_page": 3,
    "total_pages": 5,
    "next_page": 4,
    "prev_page": 2
  }
}

هكذا يمكنك أن تسهل على الـ Frontend استخدام هذه المعلومات ليعرف عدد البيانات والصفحات المتاحة وهل يوجد صفحة تالية أم لا
يمكنك تعديل معلومات الـ Pagination حسب احتياجاتك

تنوع الـ Query Parameters

وكما قلنا فإن الـ Query أوامر تكون في نهاية الـ Endpoint بعد علامة الاستفهام لإعطاء الـ Frontend بعض من أنواع التحكم على شكل البيانات الراجعة والتي يطلبها

ليستطيع عمل عمليات مخصصة للبيانات كما يحلو له، على سبيل المثال تريد جميع الأشخاص الذي يعيشون في مصر
أو تريد الأشخاص الذي يزيد راتبهم عن 3000 أو الأشخاص الذي أعمارهم 25
عمليات مخصص ومحددة مثل هذه نسميها Query

لذا يفضل دائمًا أنك كـ Backend أن تصمم بعض من الـ query parameters لتساعد من سيستخدمون الـ API بعمل تلك العمليات المعقدة بطريقة سلسة وبسيطة

البحث عن طريق بيانات معينة

فإذا كانت بيانات المستخدمين عندك تتكون من name، age، email، salaryو country فيمكنك أن تتيح للـ Frontend بعمل query ويبحث عن طريق هذه البيانات
فعلى سبيل المثال تريد الأشخاص الذين يعيشون في مصر سيكون شكل الـ query هكذا /api/users?country=Egypt هكذا سيتم احضار الأشخاص الذين يعيشون في مصر فقط
مثال آخر أحضر الأشخاص الذي اعمارهم تساوي 30, سيكون الـ query هكذا users?age=30
أو تريد الأشخاص الذين يعيشون في فلسطين الذي يزيد راتبهم عن 3000 سيكون الـ query هكذا /api/users?salary=3000&country=Palestine

هكذا تساعد من سيستخدم الـ API على القيام بعمل عمليات مخصص كما يحلوا له بسلاسة كما ترى

ترتيب النتائج

يمكنك أن تقوم بإضافة المزيد من المتغيرات التي تزيد من سلاسة وسهولة عمل العمليات المعقدة
مثل ترتيب الناتج بناءًا على عنصر أو استبعاد بعض البيانات و غيرها من الأمور

فيمكنك عمل query parameters لنسميها sort وتكون المسؤولة عن ترتيب البيانات تصاعديًا اوتنازليًا بحسب العمر او الراتب اول أي عنصر معين
مثال /api/users?sort=age هكذا سيتم إحضار جميع الأشخاص ويرتبهم تصاعديًا بحسب العمر
ان أردت ترتيب تنازلي ؟ يمكنك ان تجعله يضع ضع علامة - هكذا /api/users?sort=-age هكذا يمكنك أن تحضر جميع الأشخاص ويرتبهم تنازليًا بحسب العمر

أحضر جميع الأشخاص الذي راتبهم يساوي 3000 ورتبهم تنازلي بحسب العمر, الـ query ستكون هكذا /api/users?salary=3000&sort=-age

تحديد واستثناء بيانات معينة

يمكنك أن تضع query parameters وتسميه fields على سبيل المثال وتجعل الـ Frontend يختار خانات معينة ليحضرها فمثلا نحن نريد الإسم فقط ولا نريد باقي البيانات
في هذه الحالة يمكننا متغير ولنسميه fields ثم نجعله يختار الخانات التي يريدها

مثال /api/users?fields=name سيحضر أسماء جميع الأشخاص فقط name وسيستبعد باقي الخانات كـ age، email، salaryو country نريد الإسم والعمر فقط, تستطيع ان تجعله يختار أكثر من خانة مفصولة بـ , مثال /api/users?fields=name,age سيحضر أسماء وأعمار جميع الأشخاص

نريد كل البيانات ما عادا الراتب والدولة, هكذا - لكي نستثني بعض الخانات يمكنك أن تجعله يضع علامة users?fields=-salary,-country

ملحوظة:الـ Query شكلها وطريقتها وخواصها ومميزاتها أنت كـ Backend من يحددها
قد تقوم بعمل API لا تدعم فيه أي query من الأساس، أو تدعم بعض الأمور أو تزيد أمورًا يناسب مقتضى المشروع

تخزين البيانات التي عليها طلب مستمر بشكل مؤقت | Caching

الـ Cache من الأمور الموصى بها في بناء الـ Restful API وخصوصًا عندما يكون لديك ضغط كبير على الـ API في جلب بعض البيانات بشكل مستمر

لنفترض أنك تصمم API لموقع متخصص في بيع وعرض منتجات معينة، وعدد الزيارات لهذا الموقع يتخطى المئة ألف يوميًا ويوجد منتجات معينة عدد الزيارات لها تصل لألف زيارة كل ساعة او دقيقة

هل تتخيل أنك في كل مرة تحضر نفس المنتجات من قاعدة البيانات ألف مرة ؟

الإجابة بالتأكيد لا فهذا غير منطقي، لذا فنحن نحتاج لشيء نستطيع فيه تخزين المنتجات التي يتم طلبها بشكل مستمر
وهذا المكان يجب أن يكون جلب البيانات منه أسرع من طلبها من قاعدة البيانات
لأن العمليات على الـ Database قد تكون مكلفة إلى حد ما من ناحية السرعة أو الأداء وغيرها من الأمور
فتخيل معي أن الـ Database مكونة من أكثر من Server وعندما تقوم بعمل عملية معينة قد تحتاج إلى تعديل وعمل عمليات أو تعديلات أخرى في قواعد البيانات المختلفة، عندما يكبر المشروع فستدخل في دوامة الأنظمة الموزعة وتعدد الـ Server في كل مكان

لذا فمن غير المنطقي أن نطلب من قاعدة البيانات نفس البيانات كل ثانية لأننا هكذا نستهلك مصادرنا على شيء يتكرر باستمرار

هنا يظهر لدينا مفهوم الـ Cache وهو أنك تخزن نسخة مؤقتة للبيانات الأكثر طلبًا في ذاكرة الـ Server الأساسي أو في Server آخر وغالبًا من يكون Proxy Server أو باختصار يكون في مكان أقرب وأسرع لك من أنك تطلب البيانات من قواعد البيانات التي لديك

لاحظ هنا بعض الأمور أننا هنا نخزن البيانات بشكل مؤقت لأنها قد يتم تعديلها
بمعنى أنك لديك منتجات في قاعدة البيانات ونفس هذه المنتجات تم تخزينها في الـ Cache
ثم حصلت بعض التعديلات في هذه المنتجات في قاعدة البيانات أو تم حذفها
هكذا يجب علينا تجديد الـ Cache وتزويده بأحدث البيانات والتحديثات سواء تعديل أو حذف

لذا فهنا بعض الأمور التي يجل أن تراعيها وأنت تقوم بعمل Cache للـ Restful API

طرق حماية الـ API الخاص بك | API Security

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

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

أحب أن أكون صريح معكم لاننا في النهاية بشر وأنا كما قلت أحب أن أنشر ما اعرفه وما تعلمته لأستفيد وأفيد الآخرين ولا أدعي الكمال أو المثالية
لذا الفقرة التالية سأقول ما لدي كنوع من رمي افكاري وما اعرفه وأحثكم على قراءة المزيد عن هذا الأمر وجمع نصائح وخبرة من أناس آخرين

ملحوظة: كما قلت سابقًا هذه كلها مجرد عناوين وأمور ذكرتها وأنت مطالب بأن تبحث عنها بتعمق قليلًا ومعرفة أفضل الحلول والأمور الموصى بها في الوقت الحالي
هناك أمور أكثر مما ذكرت وتفاصيل أعمق وكل شيء يجب عمل حساب له ودراسة وغير ثابت ويختلف من مشروع لمشروع