التعامل مع الـ API

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

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

المقدمة

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

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

هل هذا ما يفعله الـ API فقط ؟

بالطبع لا، الـ API كما قلنا هو الوسيط بينك وبين مصدر البيانات او الـ Server بشكل عام

في الحياة الواقعية او في مجال العمل وليكن مجال الويب سيكون الـ API الوسيط بين الـ Frontend والـ Backend

تجهيز قاعدة بيانات افتراضية مع API

فلنفترض ان صديقنا الـ Backend اعطانا رابط API افتراضي وليكن https//database.api/users
وقاعدة البيانات تحتوي على الآتي:-

{
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]"
    }
  ]
}

على الـ Backend ان يعطيك شرح تفصيلي لكيفية استخدام هذا الـ API
فلنفترض انه اخبرنا عن الـ endpoints التي سيخدمها الـ Frontend ليتعامل مع الـ API
ولتكن هكذا:-

- Get all users
    endpoint: 'https://database.api/users'
    method: 'GET'

- Get one user
    endpoint: 'https://database.api/users/:id'
    method: 'GET'

- Add user
    endpoint: 'https://database.api/users'
    method: 'POST'
    body: {name, age, email}

- Update user (entirely)
    endpoint: 'https://database.api/users/:id'
    method: 'PUT'
    body: {name, age, email} // must send all

- Update user (partial)
    endpoint: 'https://database.api/users/:id'
    method: 'PATCH'
    body: {name, age, email} // at least send one field

- Delete user
    endpoint: 'https://database.api/users/:id'
    method: 'DELETE'

الـ Backend المحترف هو من يستطيع كتابة شرح تفصيلي وواضح لكيفية استخدام هذا الـ API ولكي لا يتسبب بازمة قلبية للـ Frontend حين يتعامل معه

الـ endpoints هي نهايات رابط الـ API فعلى سبيل المثال ان اردت كل المستخدمين فيكون الـ endpoint هو /users وان اردت شخص معين فيكون الـ endpoint هو /users/:id وهكذا
الـ Backend يحدد لك ويصف لك الـ endpoints التي سيستخدمها الـ Frontend ليتعامل مع الـ API

سنشرح كل شيء بالتفصيل اثناء الشرح وسنستخدم هذا الـ API الافتراضي اثناء الشرح

Methods / Actions

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

سنستخدم لغة javascript للتطبيق العملي لا اكثر، لكن كل ما سنشرحه هنا من أمور ومفاهيم يمكنكم عمله في أي لغة اخرى

async function handleAPI() {
  let url = 'https//database.api/users';
  let obj = await fetch(url, {
    method: 'GET', // tell the api to get data (default)
  });
}
handleAPI();

هنا عن طريق الـ method سيقوم الـ API بتنفيذ الطلب المطلوب منه
ان لم تعطيه أي method فالقيمة الافتراضية تكون GET

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

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

GET

نستخدمها لإحضار البيانات ولا تقوم باي تعديل او تغير في قيمة البيانات على الاطلاق
ولا يمكن ارسال أي بيانات من خلالها

ان كانت البيانات موجود فيكون رمز الحالة 200 أي ان البيانات تم إحضارها بنجاح، وتكون القيمة الراجعة من طلب الـ API هي البيانات المطلوبة

وان لم تكن البيانات موجود يكون رمز الحالة 404 أي ان البيانات غير موجودة والقيمة الراجعة تكون بها بيانات عن نوع الخطأ بالتفصيل وغالبا ما تكون رسالة تقول انه لم يجد البيانات

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

كيف يكون الأمر بشكل عملي ؟

سنستعمل قاعدة البيانات الافتراضية التي انشأناها وقلنا ان صديقنا الـ Backend اعطانا الـ API لها وهو https//database.api/users

انت كـ Frontend تقوم باستخدام الـ API لإحضار البيانات

تريد كل المستخدمين ؟ علينا الرجوع للتعليمات او الارشادات التي كتبها لنا الـ Backend لنستخدم الـ API

- Get all users
    endpoint: 'https://database.api/users'
    method: 'GET'

هنا سنجد انه يقول لنا لكي نحضر كل المستخدمين نستخدم الـ endpoint وهو https://database.api/users
و method يجب ان يكون نوعه GET

async function handleAPI() {
  let url = 'https//database.api/users'; // /users endpoint to get all users
  let obj = await fetch(url, {
    method: 'GET', // tell the api to get data (default)
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

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

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]"
    }
  ]
}

الـ Backend هو أيضًا المسؤول عن شكل البيانات الراجعة للـ Frontend
وهنا كما نرى انه ارسل لنا بيانات زائدة لوصف وتوضيح البيانات مثل status, code, message بجانب الـ users

تريد مستخدم معين ؟

- Get one user
    endpoint: 'https://database.api/users/:id'
    method: 'GET'

هنا ستجد ان الـ endpoint يحتوي على :id ومعنى هذا انه متغير يمكن ان يحتوي على أي رقم
والـ method يجب ان يكون نوعه GET

async function handleAPI() {
  let url = 'https//database.api/users/2'; // /users/:id endpoint to get user with id 2
  let obj = await fetch(url, {
    method: 'GET', // tell the api to get data (default)
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

الناتج سيكون المستخدم صاحب الـ id رقم 2

{
  "status": "success",
  "code": 200,
  "message": "User found",
  "user": {
    "id": 2,
    "name": "Mohamed",
    "age": 30,
    "email": "[email protected]"
  }
}

ان طلبت مستخدم غير موجود ؟

async function handleAPI() {
  let url = 'https//database.api/users/4'; // user that doesn't exist
  let obj = await fetch(url, {
    method: 'GET',
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

الناتج سيكون بيانات عن نوع الخطأ بالتفصيل يكتبها الـ Backend بنفسه ويصف فيه نوع الخطأ كما ذكرنا

{
  "status": "error",
  "code": 404,
  "message": "User not found"
}

ملخص ما تعلمناه هو:

اهم رموز الحالة في الـ GET

POST

نستخدمها عندما نريد اضافة بيانات جديدة في قاعدة البيانات خاصتنا

هنا عندما ترسل البيانات الجديدة من خلال الـ API على سبيل المثال نريد إنشاء مستخدم جديد

ان نجح الإنشاء فسيكون رمز الحالة 201 أي انه تم إنشاء البيانات بنجاح

الـ POST تستخدم أيضًا لارسال بيانات للـ Backend بشكل عام وليس شرطا ان يكون بغرض إنشاء شيء ما كإنشاء مستخدم جديد مثلًا او تخزين البيانات
قد نريد ارسال بيانات للـ Backend ليقوم بعمل بعض العمليات عليها لا غير

كيف نرسل البيانات من خلال الـ API ؟

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

هنا يأتي الـ body وهو حرفيا كما يوحي الإسم جسم الطلب الذي نخزن فيه البيانات التي نريد ارسالها مع الطلب إلى الـ Backend ليتعامل معها

كيف يكون الأمر بشكل عملي ؟

لنرجع لارشادات صديقنا الـ Backend

- Add user
    endpoint: 'https://database.api/users'
    method: 'POST'
    body: {name, age, email}

هنا سنلاحظ ان الـ endpoint الخاص بإنشاء مستخدم جديد يشبه الـ endpoint الخاص بإحضار جميع المستخدمين
والـ API يفرق بين الـ endpoint المتشابهه ويحدد نوع الطلب عن طريق الـ method
ان كان POST فسيعرف انه يجب ان يقول للـ Backend ان ينشيء مستخدم جديد وان كان GET فسيعرف انه يجب ان يحصل على جميع المستخدمين
سنلاحظ انه يقول انه يحتاج body مكون من name, age, email
والـ Backend هو من يحدد ما البيانات التي يجب على الـ Frontend ارسالها

async function handleAPI() {
  let url = 'https//database.api/users';
  let obj = await fetch(url, {
    method: 'POST',

    // send the data to the backend (json format)
    body: JSON.stringify({
      name: 'Osama',
      age: 40,
      email: '[email protected]',
    }),

    // headers contains extra information about the request
    headers: {
      'Content-Type': 'application/json',
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

لنحلل ما لدينا هنا، حددنا الـ endpoint الخاص بإنشاء مستخدم جديد والـ method جعلناها POST

الآن لدينا الـ body يحتوي على البيانات التي نريد ارسالها للـ Backend بصيغة json

// send the data to the backend (json format)
body: JSON.stringify({
    name: 'Osama',
    age: 40,
    email: '[email protected]'
}),

يمكنك ان تحدد شكل وصيغة البيانات سواء نص عادي او json وغيرها من الصيغ، هذا شيء يتفق عليه الـ Backend معك
لكن غالب الأمر يكون بصيغة json وهو الشكل المتعارف عليه في ارسال البيانات من الـ Frontend إلى الـ Backend

الآن سنلاحظ شيء جديد ظهر معنا وهو الـ headers

// headers contains extra information about the request
headers: {
    'Content-Type': 'application/json'
}

الـ headers يحتوى على معلومات اضافية ترسل مع الطلب
من تلك المعلومات الاضافية لدينا Content-Type نكتب فيه نوع البيانات التي ارسلناها للـ Backend في الـ body
وبما ان البيانات نوعها json نكتب application/json هكذا وضحنا ان نوع البيانات سيكون json
يوجد قيم كثيرة لوصف البيانات في الـ Content-Type بمختلف صيغها ومنها

سنتطرق لشرح مفصل عن الـ headers فيما بعد في هذه المقالة

ملحوظة: الـ Content-Type نكتبه عندما نرسل بيانات في الـ body مثل الـ POST, PUT, PATCH او أي طلب يحتوى على بيانات سترسل إلى الـ Backend

حسنًا بعد ان ارسلنا البيانات إلى الـ Backend ستلاحظ اننا هنا نطبع قيمة راجعة من الطلب مع اننا فقط نرسل بيانات إلى الـ Backend لا غير

let data = await obj.json();
console.log(data);

كما قلنا انه اذا نجح طلب الـ POST او فشل كيف لنا ان نعرف ؟
هنا الـ Backend يقوم بارجاع لنا معلومات توضح وتصف الحالة

في حالة النجاح سيكون ناتج الطباعة كهذا على سبيل المثال

{
  "status": "success",
  "code": 201,
  "message": "User created successfully"
}

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

اهم رموز الحالة في الـ POST

PUT

نستخدمها عندما نريد ان نعدل بيانات عنصر ما في قاعدة البيانات

ان نجح التعديل فسيكون رمز الحالة 200 أي انه تم تعديل البيانات بنجاح وتخزينها

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

كيف يكون الأمر بشكل عملي ؟

فلنفترض اننا نريد ان نعدل بيانات المستخدم رقم 1

{
  "id": 1,
  "name": "Ahmed",
  "age": 25,
  "email": "[email protected]"
}

فالنقل اننا نريد ان نغير العمر ونجعله 30

لنرجع لارشادات صديقنا الـ Backend

- Update user (entirely)
    endpoint: 'https://database.api/users/:id'
    method: 'PUT'
    body: {name, age, email} // must send all

حسنًا الـ endpoint يحتوى على :id وهو رقم المستخدم الذي نريد تعديله

ثم سنلاحظ انه يقول انه يجب ان نرسل جميع بيانات المستخدم ان اردنا ان نعدل شيء

async function handleAPI() {
  let url = 'https//database.api/users/1';
  let obj = await fetch(url, {
    method: 'PUT',

    body: JSON.stringify({
      name: 'Ahmed',
      age: 30, // new value
      email: '[email protected]',
    }),

    headers: {
      'Content-Type': 'application/json',
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

هنا نكتب جميع بيانات المستخدم في الـ body مع القيم الجديدة وفي حالتنا سنجعل العمر يساوي 30
نقول له ان الـ method سيكون نوعه PUT ثم نقوم بتحديد نوع البيانات في الـ Content-Type ونجعلها application/json

{
  "status": "success",
  "code": 200,
  "message": "User updated successfully",
  "user": {
    "id": 1,
    "name": "Ahmed",
    "age": 30,
    "email": "[email protected]"
  }
}

هنا الـ Backend يقول انه تم تعديل البيانات بنجاح وتخزينها وارسل لنا بيانات المستخدم الجديدة بعد التعديل

مشاكل حين نتعامل مع الـ PUT

ستلاحظ شيء هنا انه نحن فقط كنا نريد ان نعدل العمر لا غير، لكنا قمنا بارسال جميع البيانات
تخيل معي ان كانت قاعدة البيانات كبيرة والمستخدم يحتوى على بيانات متعددة منها الإسم والعمر والبريد الالكتروني والجنسية وعنوانه ومكان عمله وراتبه وغيرها...

ونحن نريد فقط تعديل شيء واحد فقط، ان استخدمنا الـ PUT فيجب ان نرسل جميع البيانات كاملة
هذه قد تكون مشكلة كبيرة بحد ذاتها

وماذا سيحدث ان لم نرسل جميع البيانات في الـ body ؟
فالنقوم بعمل نفس الشيء لكن سنرسل في الـ body فقط البيانات التي نريد تعديلها وهو الـ age: 30

body: JSON.stringify({
    age: 30, // new value
}),

هل سيفشل الطلب ؟
لا الطلب سينجح لكن دعونا نرى الرد الذي ارسله الـ Backend لنا

{
  "status": "success",
  "code": 200,
  "message": "User updated successfully",
  "user": {
    "id": 1,
    "age": 30
  }
}

سنجد ان المستخدم لم يعد لديه إسم او بريد الكتروني

في حقيقة الأمر الـ PUT يقوم باستبدال البيانات الجديدة المرسلة في الـ body بالبيانات القديمة للمستخدم
يستبدلها بالكامل!، بالتالي ان ارسلنا بيانات ناقصة في الـ body فلن يقوم بوضعها في البيانات الجديدة وسيقوم بحذفها بكل بساطة

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

اهم رموز الحالة في الـ PUT

PATCH

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

كيف يكون الأمر بشكل عملي ؟

الأمر سيكون مشابه للـ PUT
دعونا نرى ماذا يقول صديقنا الـ Backend

- Update user (partial)
    endpoint: 'https://database.api/users/:id'
    method: 'PATCH'
    body: {name, age, email} // at least send one field

حسنًا الـ endpoint يحتوى على :id وهو رقم المستخدم الذي نريد تعديله

سنلاحظ انه يقول هنا انه على الاقل نرسل شيءً واحدًا

async function handleAPI() {
  let url = 'https//database.api/users/1';
  let obj = await fetch(url, {
    method: 'PATCH',

    body: JSON.stringify({
      name: 'Ahmed_Tab', // new value
    }),

    headers: {
      'Content-Type': 'application/json',
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

لاحظ اننا ارسلنا في الـ body ما نريده تعديله وهو الإسم الجديد

هنا سيتم تعديل الإسم فقط لا غير
وباقي بيانات المستخدم ستظل كما هي ولن يتم مسحها كما حدث مع الـ PUT

رد الـ Backend لنا سيكون كالتالي

{
  "status": "success",
  "code": 200,
  "message": "User updated successfully",
  "user": {
    "id": 1,
    "name": "Ahmed_Tab",
    "age": 30,
    "email": "[email protected]"
  }
}

بما ان الـ Backend ارجع لنا البيانات كاملة بهذا نتأكد انه لم يتم مسح أي بيانات قديمة

ملحوظة: يفضل دائمًا عند التعديل استخدام الـ PATCH بدلًا من الـ PUT في حالة ضمان أن الذي تريد تعديله موجود في قاعدة البيانات بالفعل، لان الـ PATCH تتعامل مع ما هو موجود بالفعل، أما إن كان الذي تريد تعديله غير موجود فاستعمل الـ PUT أول الـ POST وأرسل البيانات كامله لأنهما إن لم يجدا الشيء الذي تريد تعديله فسيقومان بإنشاءه من جديد

اهم رموز الحالة في الـ PATCH

DELETE

بديهي ان نستخدم الـ DELETE لحذف عنصر من قاعدة البيانات

لا يحتوى على body بحكم انك تحذف بيانات ولا ترسل شيء للـ Backend

كيف يكون الأمر بشكل عملي ؟

بدأنا نعتاد على الأمر لانه اصبح يتكرر ومتشابه في العديد من الحالات

دعونا نرى ماذا يقول صديقنا الـ Backend

- Delete user
    endpoint: 'https://database.api/users/:id'
    method: 'DELETE'

هنا هو يريد فقط id المستخدم الذي نريد حذفه فالنقل اننا نريد حذف المستخدم رقم 1

async function handleAPI() {
  let url = 'https//database.api/users/1';
  let obj = await fetch(url, {
    method: 'DELETE',
  });
  let data = await obj.json();

  console.log(data);
}

لاحظ انه لا يوجد body ولا حتى Content-Type لاننا لا نرسل شيء وهذا شئ بديهي

ان نجح سيرسل لنا الـ Backend بيانات انه تم الحذف بنجاح

{
  "status": "success",
  "code": 200,
  "message": "User deleted successfully",
  "user": {
    "id": 1,
    "name": "Ahmed_Tab",
    "age": 30,
    "email": "[email protected]"
  }
}

اهم رموز الحالة في الـ DELETE

Query

حسنا ما هو الـ Query؟

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

لنفترض أن قاعدة البيانات خاصتنا أصبحت بهذا الشكل

{
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Palestine"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]",
      "salary": 4000,
      "country": "Egypt"
    },
    {
      "id": 4,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 5,
      "name": "Osama",
      "age": 30,
      "email": "[email protected]",
      "salary": 5000,
      "country": "Palestine"
    }
  ]
}

سوف نقوم باستكمال شرح الـ query على قاعدة البيانات تلك
على نفس الرابط https//database.api/users

ملحوظة: يجب على الـ Backend أن يجعل الـ API يمكن القيام بعمل query عليه
لأنه ليس كل API يقبل بالـ query الأمر عائد للـ Backend ليجعله يقبل بالـ query ام لا

كيف نقوم بعمل Query على الـ API ؟

نقوم بها عن طريق أننا نختار الـ endpoint الذي نريد أن نقوم بعمل عليه عملية الـ query ثم نكتب بعده علامة استفهام ? ثم نكتب الـ query
الأمر يبدو كهذا /endpoint?key=value

يمكنك أن ترى تطبيقًا عمليا على هذا في محرك البحث جوجل
حاول أن تبحث عن كلمة تعلم البرمجة بالعربي على سبيل المثال
أنظر الى الرابط ستجده هكذا

https://www.google.com.eg/search?q=تعلم+البرمجة+بالعربي

ستجد أن لديهم endpoint تدعى search ثم قام بعمل عليها query هنا الـ key هو q والقيمة هى التي كتبناها في البحث تعلم البرمجة بالعربي

في حالتنا سيكون الـ users ثم نكتب بعده علامة استفهام ? ثم نكتب الـ query
ثم نكتب الـ query

مثال نريد الأشخاص الذين يعيشون في مصر سيكون شكل الـ query هكذا

async function handleAPI() {
  let url = 'https//database.api/users?country=Egypt'; // query
  let obj = await fetch(url, {
    method: 'GET',
  });
  let data = await obj.json();

  console.log(data);
}

لاحظ أننا نقوم بعمل طلب عن طريق الـ API بشكل اعتيادي
لكن في نهاية الـ endpoint كتبنا الـ query بعد الـ ? هكذا users?country=Egypt

وهذا معناه كالأتي أحضر لي كل المستخدمين الذين في مصر

الناتج سيكون هكذا

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]",
      "salary": 4000,
      "country": "Egypt"
    },
    {
      "id": 4,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    }
  ]
}

لاحظ انه أحضر فقط ثلاث أشخاص وهم الذين يعيشون في مصر فقط، لأنهم يحققون الشرط الذي وضعناه users?country=Egypt

لنقم بعمل مثال آخر أحضر الأشخاص الذي اعمارهم تساوي 30

سيكون الـ query هكذا users?age=30

لا داعي لكتابة الكود لانك فهمت الأمر بالفعل

ناتج الـ query سيكون هكذا

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Palestine"
    },
    {
      "id": 5,
      "name": "Osama",
      "age": 30,
      "email": "[email protected]",
      "salary": 5000,
      "country": "Palestine"
    }
  ]
}

تم إحضار شخصين عمرهم 30 ويحققان الشرط بالفعل

عمل Query بأكثر من شرط

يمكننا ان نحدد أكثر من شرط عن طريق فصل كل شرط ب & هكذا users?salary=3000&country=Palestine

الناتج

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Palestine"
    }
  ]
}

تم إحضار الناتج الذي حقق الشروط

sort=value - ترتيب بيانات الـ Query

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

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

أحضر جميع الأشخاص الذي راتبهم يساوي 3000 ورتبهم تنازلي بحسب العمر

الـ query ستكون هكذا users?salary=3000&sort=-age

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 4,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Palestine"
    },
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    }
  ]
}

fields=value1, value2 - اختيار خانات معينة

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

مثال users?fields=name سيحضر أسماء جميع الأشخاص

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed"
    },
    {
      "id": 2,
      "name": "Mohamed"
    },
    {
      "id": 3,
      "name": "Ali"
    },
    {
      "id": 4,
      "name": "Omar"
    },
    {
      "id": 5,
      "name": "Osama"
    }
  ]
}

نريد الإسم والعمر فقط نستطيع اختيار أكثر من خانة مفصولة بـ ,

مثال users?fields=name,age سيحضر أسماء وأعمار جميع الأشخاص

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35
    },
    {
      "id": 4,
      "name": "Omar",
      "age": 35
    },
    {
      "id": 5,
      "name": "Osama",
      "age": 30
    }
  ]
}

نريد كل البيانات ما عادا الراتب والدولة

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

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]"
    },
    {
      "id": 4,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]"
    },
    {
      "id": 5,
      "name": "Osama",
      "age": 30,
      "email": "[email protected]"
    }
  ]
}

page=n&limit=m - تقييد عدد النواتج

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

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

هذا العدد يحدده الـ Backend بالطبع
وتستطيع التحكم بهذا العدد عن طريق limit
فإن كنا نريد أول شخصين فقط فسنحصل على أول شخصين فقط
مثال users?limit=2

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 1,
      "name": "Ahmed",
      "age": 25,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    },
    {
      "id": 2,
      "name": "Mohamed",
      "age": 30,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Palestine"
    }
  ]
}

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

يمكنك أن تتخيل أن limit كأنها تقسم الناتج لصفحات و page تختار لك الصفحة التي تريدها

تريد المجموعة الثانية ؟

أظنك توقعت شكل الـ query بالفعل users?limit=2&page=2

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 3,
      "name": "Ali",
      "age": 35,
      "email": "[email protected]",
      "salary": 4000,
      "country": "Egypt"
    },
    {
      "id": 4,
      "name": "Omar",
      "age": 35,
      "email": "[email protected]",
      "salary": 3000,
      "country": "Egypt"
    }
  ]
}

تريد المجموعة الثالثة ؟

users?limit=2&page=3

{
  "status": "success",
  "code": 200,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": 5,
      "name": "Osama",
      "age": 30,
      "email": "[email protected]",
      "salary": 5000,
      "country": "Palestine"
    }
  ]
}

حصلنا على شخص واحد لان لدينا 5 أشخاص فقط في قاعدة البيانات
فبعد ما استخدمنا limit=2 فسمنا الأشخاص بهذا الشكل (1, 2) (3, 4) (5) والمجموعة الثالثة هنا بها شخص واحد

بالطبع إن تجاوزت العدد المحدد فمثلا كتبت page=50 فغالبا ستحصل على بيانات فارغة

إرسال Array و Object في الـ Query

ملحوظة: هذا الجزء من الشرح سيكون مناسب للـ Frontend ليدرك كيف يستقبل الـ Backend الـ query باشكالها المختلفة

هناك شكل متقدم قليلًا لشكل البيانات التي نرسلها في الـ query
وهي اننا نستخدم اقواص الأراي في الـ query بهذا الشكل users?name[]=value

ما معنى هذا الشكل ؟
أولًا قبل أن نجيب، هل فكرت كيف يستقبل صديقك الـ Backend الـ query ؟

سنمسك شكل شكل ونحلله قليلًا

  1. أبسط شكل للـ query هو الشكل المعتاد لنا وهو هذا الشكل البسيط:
    users?name=value

صديقك الـ Backend سيستقبلها بهذا الشكل

{
  "name": "value"
}

هذا فقط هو الشكل الاعتيادي لما يستقبله الـ Backend
لا أظن ان هاك كلام نستطيع ان نضيفه هنا

  1. أما عندما ترسلها مع [] بهذا الشكل:
    users?name[]=value
{
  "name": ["value"]
}

ستلاحظ أنه هنا استقبل القيمة في أراي أراي هذه المرة
بمعنى أنه بوجود [] فإن القيمة سترسل إلى الـ Backend داخل أراي

  1. إرسال أكثر من عنصر داخل الأراي:
    users?name[]=value1&name[]=value2&name[]=value3
{
  "name": ["value1", "value2", "value3"]
}

ميزة وضع [] مع الـ key سمحت لنا بإرسال أكثر من قيمة لنفس الـ key
فهنا كما ترى بما أن name[] اصبح أراي
فيمكننا اضافة اكثر من عنصر فيه، بارسال اكثر من قيمة يحملون نفس الـ key
كما رأيت في الـ query السابقة

  1. إرسال Object في الـ Query:
    users?filter[key]=value

هنا لن يتحول الـ filter إلى أراي لأن عندما تستقبل الـ [] أي key هنا ستتحول لـ object

{
  "filter": { "key": "value" }
}

كما ترى فالـ filter اصبحت بالنسبة للـ Backend عبارة عن الـ object يحمل key بقيمة معينة

  1. إرسال أكثر من عنصر لنفس الـ Key داخل الـ Object:
    users?filter[key]=value1&filter[key]=value2&filter[key]=value3

عندما نرسل قيم مختلفة لنفس الـ key ماذا سيحدث برأيك ؟
الأمر سيكون مشابه للرقم 3

{
  "filter": {
    "key": ["value1", "value2", "value3"]
  }
}

عندما نثبت الـ key ونرسل اكثر من قيمة له
فسيتحول هذا الـ key لأراي كما ترى
وسيظل الـ filter كما هو object يضم هذا الـ key

  1. إرسال أكثر من Key داخل الـ Object:
    users?filter[key1]=value&filter[key2]=value&filter[key3]=value

حسنًا أظن هذه ستكون سهلة وتستطيع ان تستنتج شكلها

{
  "filter": {
    "key1": "value",
    "key2": "value",
    "key3": "value"
  }
}

بطبع لاننا أنشأنا keys مختلفة فستكون الـ filter عبارة عن object يضم هذه الـ keys المختلفة
وكل key بالطبع سيحتوي على القيمة التي استندت له

هل هذا كل شيء في الـ Query ؟

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

كل API قد تجد الـ query خاصته مختلفة تمامًا لذا عليك دائمًا الرجوع لتعليمات الـ Backend قبل استخدامك للـ API

Headers

دعونا نتكلم عن الـ Headers بشيء من التفاصيل
ذكرنا ان الـ headers يحتوى على معلومات اضافية ترسل مع الطلب

تلك المعلومات الإضافية قد تكون أي شيء تريد إرسالها إلى الـ Backend

على سبيل المثال أريد إرسال أسمي في الـ header وأن أرسل رسالة I love you, backend man
فأقوم بكتابتها في الـ headers بهذا الشكل

headers: {
  name: 'Ahmed',
  message: 'I love you, backend man'
}

تلك البيانات سترسل إلى الـ backend بنجاح،
لكن هل الـ backend يحتاجُها ؟ لا، غالبًا لن يعمل معك مجددًا وسيقدم استقالته

عليك دائمًا إرسال البيانات التي يطلب الـ backend منك إرسالها في الـ headers الأمر مشابه للـ body لكن مع بعض الخواص

Content-Type

من تلك البيانات التي يمكننا إرسالها في الـ headers لدينا Content-Type ونكتب فيها نوع البيانات التي ارسلناها في الـ body

body: JSON.stringify({
  name: 'Ahmed',
  age: 30,
  email: '[email protected]',
}),

headers: {
  'Content-Type': 'application/json',
},

هنا ارسلنا للـ Backend بيانات في الـ body على شكل JSON لذا كان يجب ان نوضح في الـ headers نوع البيانات التي نريد ارسالها
لذا كتبنا داخل الـ headers نوع البيانات 'Content-Type': 'application/json'
إن كانت البيانات المرسلة في الأ body نوعها HTML فقط نكتب 'Content-Type': 'text/html' وإن كانت XML نكتب 'Content-Type': 'text/xml' أو 'Content-Type': 'application/xml' ... وهكذا

يوجد قيم كثيرة لوصف البيانات في الـ Content-Type وهذه بعضها

text/html
text/plain
application/json
images/jpeg
images/png
audio/mp3
video/mp4
... ect

نكتب تلك القيم في الـ Content-Type بحسب نوع البيانات التي نريد ارسالها في الـ body

content-type هي كلمة متعارف عليها في الـ headersبأنها تحدد نوع البيانات التي سترسل إلى الـ backendفي الـ body, لذا لا تستخدمها لإرسال بيانات أخرى عشوائية غير البيانات التي من المفترض انها تتعامل معها

Authorization

يحتوي الـ headers على بيانات أخرى مثل بيانات الـ Authorization ليعرف الـ Backend هل لديك صلاحية لاستخدام هذا الـ API ام لا

في غالب الأمر عندما ينشيء الـ Backend الـ API يضع له رمز أمان ندعوه بـ API-KEY وهو رمز يضعه الـ الـ Backend بشكل معين لكي لا يتمكن أحد من استخدام الـ API
من لديهم الرمز هم فقط من يستطيعون استخدام الـ API

Unauthorized 401

لنفرض ان صديقنا الـ Backend وجد نفسه متفرغا فقرر ان يضع API-KEY للـ API
وليكن بهذا الشكل

"API-KEY": "ajmad-sharh-api-fe-el-3alam"

فأنت يا عزيزي الـ Frontend تقوم بكل أمان الله بعمل طلب GET بسيط على الـ API فتفاجئ بهذا الرد

{
  "status": "error",
  "code": 401,
  "message": "Unauthorized, You are not allowed to access this resource"
}

بأنك لا تملك صلاحية لاستخدام هذا الـ API، ويكون رمز الحالة 401 ومعناه انه تم انتهاء صلاحيتك لاستخدام هذا الـ API هنا تذهب وتصرخ للـ Backend عن سبب هذا فيعطيك الـ Backend الرمز ويقول لك ضع هذا الرمز في الـ headers تحت مسمى API-KEY

فتذهب وتعيد كتابة الكود بهذا الشكل

async function handleAPI() {
  let url = 'https//database.api/users';
  let obj = await fetch(url, {
    method: 'GET',
    headers: {
      'API-KEY': 'ajmad-sharh-api-fe-el-3alam', // add API-Key in the headers
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

عند ارسال الطلب سيذهب الـ Backend إلى الـ headers ويبحث عن الرمز المرسل في الـ API-KEY ويقارنه ان كان الرمز صالحًا سيتم تنفيذ الطلب بنجاح وان لم يكن الرمز صالحًا سيتم رفض الطلب ويرسل لك انك لا تملك الصلاحية للوصول إلى هذه البيانات

لذا ان كان الـ API الذي تستخدمه محمي من API-KEY عليك ان تحرص على كتابته في كل طلب ترسله في الـ API

عليك ايضًا ان تحرص على استخدام المسمى الذي حدده لنا الـ Backend وهو الـ API-KEY وهذا المسمى يحدده الـ Backend قد يكون باي اسم اخر قد يكون API KEY أو Key أو API Token أو أي شيء انه مجرد مسمى يختاره الـ Backend لا أكثر ولا أقل

أحيانًا ينتهي صلاحية الـ API-KEY لانه يملك تاريخ انتهاء للصلاحية يحددها الـ Backend ففي تلك الحالة عليك طلب رمز جديد من الـ Backend وأحيانًا يكون أبدي أي ليس له تاريخ انتهاء الصلاحية

Forbidden 403

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

async function handleAPI() {
  let url = 'https//database.api/users';
  let obj = await fetch(url, {
    method: 'DELETE',
    headers: {
      'API-KEY': 'ajmad-sharh-api-fe-el-3alam',
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

فيفاجئ بهذه الرسالة

{
  "status": "error",
  "code": 403,
  "message": "Forbidden, You are not allowed to do this operation"
}

ما معنى هذا ؟ برغم بأنه استخدم الـ API-KEY بشكل صحيح إلا انه لم يكن مصرح له تنفيذ عملية المسح بمعنى انه يمكن ان تكون تملك الـ API-KEY لاستخدام بعض العمليات على الـ API وليس كلها
هناك عمليات قد تكون محظورة عليك برغم من انك تملك الـ API-KEY
فعندما تحاول القيام بعمليات غير المصرح لك استعمالها فسيقوم صديقك الـ Backend بإرسال رمز الحالة 403 أي أنت غير مصرح لك أن تستخدم هذه العملية

إذا كان من يملك الـ API-KEY لا يمكنه القيام بكل العمليات، حسنًا إذا كنا نريد القيام بكل العمليات ماذا نفعل ؟

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

هذه الطريقة تجعل الـ Backend يقوم بعمل أكثر من API-KEY

Token

الحل الآخر هو ان يقوم الـ Backend بعمل token بجانب الـ API-KEY ويعطي token لكل شخص يستخدم الـ API ويعطي صلاحيات معينة لكل token على حسب وظيفة الشخص الذي يستخدم الـ API هل هو شخص عادي او شخص مسؤول عن عمليات حساسة ومهمة فنعطيه صلاحيات أكثر وهكذا

الـ token يقوم الـ Frontend بتخزينه في الـ الـ LocalStorage ويضعه في الـ headers مع كل Request عندما يرسل أي Request أو يمكننا أن نخزنه في الـ cookies الأمر عائد لكما كـ Frontend و Backend كيف تديران الأمر
الـ token أيضًا قد ينتهي صلاحية استخدامه ويحتاج إلى التجديد من قبل الـ Backend

هذه الطريقة تجعل الـ Backend يقوم بعمل API-KEY واحد فقط يستخدمه الـ Frontend ويتم التفريق بين المستخدمين وصلاحيتهم داخل التطبيق بالـ token

مثال الشركة (Unauthorized vs Forbidden)

دعونا نسترجع الفرق بين الـ Unauthorized والـ Forbidden بمثال تخيل ان هناك شركة ولنمثل هذه الشركة بالـ API وانت شخص عادي تريد دخول الشركة، وعندما حاولت الدخول وجدت البواب يقول لك أنت لست موظف في الشركة وهذا هو الـ Unauthorized أنك لست مصرح لك من الأساس بدخول الشركة وفي تلك الحالة عليك يجب ان يتم توظيفك في الشركة وتحصل على بطاقة شخصية تثبت أنك موظف في الشركة
هذه البطاقة تمثل الـ API-KEY بعد ما حصلت على الوظيفة والبطاقة ودخلت الشركة بنجاح
فأنت مصرح لك بدخول الشركة وان تعمل في بعض الأمور فيها أي انك مصرح لك ان تستخدم الـ API وتقوم بعمل بعض العمليات عليها

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

هذا هو الـ Forbidden أي أنه برغم من أنك موظف في الشركة لكن هناك بعض الأمور المحظورة عليك بما أنك موظف عادي
وتلك الأمور للموظفين الأعلى رتبة فقط
رتبتك تمثل الـ token فالرتبة تحدد لك صلاحياتك في الشركة

فهكذا في الـ API هناك عمليات لا تملك الصلاحية للقيام بها لأن الـ token الخاص بك لا يملك صلاحيات كافية لهذه العملية

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

async function handleAPI() {
  let url = 'https//database.api/users';
  let obj = await fetch(url, {
    method: 'DELETE',
    headers: {
      'API-KEY': 'ajmad-sharh-api-fe-el-3alam',
      // the token is stored in the cookies or localStorage
    },
  });
  let data = await obj.json();

  console.log(data);
}
handleAPI();

وهكذا تم رفضك من الشركة وأصبحت مشردًا هنا وهناك لذا أحرص دائما على إخفاء الـ API-KEY والـ token والخاص بك