دليل شامل لواجهة برمجة تطبيقات REST (REST API): العميل، الخدمة، والاستدعاءات مع أمثلة برمجية
مقدمة إلى عالم واجهات برمجة التطبيقات (APIs)
هل تساءلت يوماً كيف تعمل عمليات تسجيل الدخول والاشتراك في المواقع الإلكترونية في الكواليس؟ أو كيف تحصل على نتائج فورية عند البحث عن مقاطع فيديو معينة على يوتيوب، وتتمكن من بثها بسلاسة من خادم بعيد؟ هذه العمليات السحرية تتم بفضل واجهات برمجة التطبيقات (APIs)، وتحديداً واجهات برمجة التطبيقات التي تتبع نمط REST.
في هذا الدليل الموجه للمبتدئين، سنأخذك في رحلة شيقة لإنشاء واجهة برمجة تطبيقات من نوع RESTful API. سنقوم بتبسيط المفاهيم المعقدة، ونتعمق في كيفية بناء خادم باستخدام بيئة التشغيل Node.js. لننطلق في استكشاف أعمق للغة JavaScript!
ما هو نمط REST؟ فك شفرة المصطلحات التقنية
قبل الخوض في التفاصيل، دعنا نوضح ما يعنيه مصطلح REST. وفقاً لموسوعة ويكيبيديا، REST (اختصار لـ Representational State Transfer) هو نمط معماري للبرمجيات يحدد مجموعة من القيود المستخدمة لإنشاء خدمات الويب. تتيح خدمات الويب من نوع RESTful للأنظمة الطالبة الوصول إلى تمثيلات نصية لموارد الويب ومعالجتها باستخدام مجموعة موحدة ومحددة مسبقاً من العمليات عديمة الحالة.
لتبسيط هذا التعريف، يمكن القول إن REST هو في الأساس مجموعة من القواعد أو المبادئ التي تحكم كيفية تواصل العميل (مثل متصفح الويب أو تطبيق الهاتف) مع الخادم. هذه القواعد تضمن أن يكون التواصل فعالاً، قابلاً للتوسع، وموثوقاً به. هناك عدة قيود أساسية تحدد نمط REST:
- هندسة العميل-الخادم (
Client-Server Architecture): يجب فصل واجهة المستخدم الخاصة بالموقع أو التطبيق عن جزء طلب البيانات وتخزينها. هذا الفصل يتيح لكل جزء أن يتوسع بشكل مستقل، مما يعزز المرونة والأداء. - عديم الحالة (
Statelessness): يجب ألا يحتفظ الخادم بأي سياق خاص بالعميل بين الطلبات. هذا يعني أن كل طلب يتم إرساله إلى الخادم يجب أن يحتوي على جميع البيانات الضرورية لإكماله، ولا يجب افتراض أن الخادم لديه أي بيانات من الطلبات السابقة. هذه الخاصية تزيد من موثوقية النظام وقابليته للتوسع. - نظام الطبقات (
Layered System): يجب ألا يتمكن العميل من معرفة ما إذا كان يتواصل مباشرة مع الخادم أو مع وسيط (مثل خادم وكيلproxyأو موازن تحميلload balancer). تسمح هذه الخوادم الوسيطة بزيادة قابلية التوسع والأمان للخادم الأساسي.
المصطلحات الأساسية في عالم REST
الآن بعد أن فهمت ما هي خدمات RESTful، دعنا نوضح بعض المصطلحات الشائعة:
عميل REST (REST Client)
هو أي برنامج أو تطبيق يمكنه الوصول إلى خدمات REST. أنت تستخدم واحداً الآن! نعم، المتصفح يمكن أن يعمل كعميل REST غير متحكم به (الموقع هو من يدير طلبات المتصفح). لفترة طويلة، استخدم المتصفح دالة مدمجة تسمى XMLHttpRequest لجميع طلبات REST. ولكن، تم استبدالها لاحقاً بـ FetchAPI، وهو نهج حديث يعتمد على الوعود (promise-based) للتعامل مع الطلبات. أمثلة أخرى تشمل مكتبات برمجية مثل axios و superagent و got، أو تطبيقات مخصصة مثل Postman (أو نسخته عبر الإنترنت postwoman!)، أو أداة سطر الأوامر مثل cURL!
خدمة REST (REST Service)
هي ببساطة الخادم الذي يقدم البيانات. هناك العديد من المكتبات الشهيرة التي تجعل إنشاء هذه الخوادم أمراً سهلاً، مثل ExpressJS لـ Node.js و Django للغة Python.
واجهة برمجة تطبيقات REST (REST API)
تحدد هذه الواجهة نقطة النهاية (endpoint) والطرق المسموح بها للوصول إلى البيانات أو إرسالها إلى الخادم. سنتحدث عن هذا بتفصيل كبير أدناه. من البدائل الأخرى لـ REST API نذكر GraphQL و JSON-Pure و oData.
كيف يعمل REST في الواقع؟
بشكل عام، أنت تطلب من الخادم بيانات معينة أو تطلب منه حفظ بعض البيانات، ويستجيب الخادم لهذه الطلبات. من الناحية البرمجية، هناك نقطة نهاية (endpoint) وهي عبارة عن عنوان URL ينتظر الخادم تلقي طلبات عليها. نتصل بنقطة النهاية هذه ونرسل بعض البيانات (تذكر، REST عديم الحالة، لا يتم تخزين أي بيانات عن الطلب) ويستجيب الخادم بالرد الصحيح.
الكلام النظري قد يكون مملاً، دعني أقدم لك عرضاً عملياً. سأستخدم أداة Postman لعرض الطلب والاستجابة:

البيانات المعادة تكون بتنسيق JSON (JavaScript Object Notation) ويمكن الوصول إليها مباشرة. هنا، https://official-joke-api.appspot.com/random_joke يُطلق عليه نقطة نهاية (endpoint) لواجهة برمجة تطبيقات (API). سيكون هناك خادم يستمع على نقطة النهاية هذه للطلبات المشابهة للطلب الذي قمنا به.
تشريح طلب REST: مكوناته الأساسية
الآن بعد أن عرفنا أن العميل يمكنه طلب البيانات وأن الخادم سيستجيب بشكل مناسب، دعنا نتعمق في كيفية تشكيل الطلب.
نقطة النهاية (Endpoint)
لقد أخبرتك بالفعل عن هذا. للتذكير، هي عنوان URL حيث يستمع خادم REST للطلبات.
الطريقة (Method)
في وقت سابق، ذكرت أنه يمكنك إما طلب البيانات أو تعديلها، ولكن كيف سيعرف الخادم نوع العملية التي يريد العميل تنفيذها؟ يطبق REST طرقاً متعددة لأنواع مختلفة من الطلبات، والأكثر شيوعاً هي:
GET: لجلب مورد من الخادم.POST: لإنشاء مورد جديد على الخادم.PATCHأوPUT: لتحديث مورد موجود على الخادم.DELETE: لحذف مورد موجود من الخادم.
الرؤوس (Headers)
هي تفاصيل إضافية يتم توفيرها للتواصل بين العميل والخادم (تذكر، REST عديم الحالة). بعض الرؤوس الشائعة هي:
رؤوس الطلب (Request Headers):
host: عنوانIPالخاص بالعميل (أو من حيث نشأ الطلب).accept-language: اللغة التي يفهمها العميل.user-agent: بيانات حول العميل، نظام التشغيل، والشركة المصنعة.
رؤوس الاستجابة (Response Headers):
status: حالة الطلب أو رمزHTTP.content-type: نوع المورد الذي أرسله الخادم.set-cookie: لتعيين ملفات تعريف الارتباط (cookies) بواسطة الخادم.
البيانات (Data)
(تسمى أيضاً body أو message) تحتوي على المعلومات التي تريد إرسالها إلى الخادم.
حان وقت البرمجة: بناء خدمة REST باستخدام Node.js
كفى تفاصيل نظرية – لننتقل إلى الكود. سنبدأ ببرمجة خدمة REST في Node.js. سنقوم بتطبيق كل ما تعلمناه أعلاه. سنستخدم أيضاً ميزات ES6+ لكتابة خدمتنا.
تأكد من تثبيت Node.js وأن أوامر node و npm متاحة في مسارك (path). سأستخدم Node 12.16.2 و NPM 6.14.4.
أنشئ دليلاً باسم rest-service-node وانتقل إليه:
mkdir rest-service-node
cd rest-service-node
قم بتهيئة مشروع Node:
npm init -y
العلامة -y تتخطى جميع الأسئلة. إذا كنت ترغب في ملء الاستبيان بالكامل، فما عليك سوى تشغيل npm init.
دعنا نثبت بعض الحزم. سنستخدم إطار عمل ExpressJS لتطوير خادم REST. قم بتشغيل الأمر التالي لتثبيته:
npm install --save express body-parser
ما فائدة body-parser؟ بشكل افتراضي، لا يستطيع Express التعامل مع البيانات المرسلة عبر طلب POST بتنسيق JSON. يسمح body-parser لـ Express بالتغلب على هذا القيد.
أنشئ ملفاً باسم server.js وأضف الكود التالي:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.json());
app.listen(5000, () => {
console.log(`Server is running on port 5000.`);
});
السطران الأولان يقومان باستيراد Express و body-parser. السطر الثالث يقوم بتهيئة خادم Express وتعيينه لمتغير يسمى app. السطر app.use(bodyParser.json()); يقوم بتهيئة إضافة body-parser. أخيراً، نقوم بتعيين خادمنا للاستماع على المنفذ 5000 للطلبات.
جلب البيانات من خادم REST: طلب GET
لجلب البيانات من الخادم، نحتاج إلى طلب GET. أضف الكود التالي قبل app.listen:
const sayHi = (req, res) => {
res.send("Hi!");
};
app.get("/", sayHi);
لقد أنشأنا دالة sayHi التي تأخذ معاملين req و res (سأشرحهما لاحقاً) وترسل رسالة ‘Hi!’ كاستجابة. تأخذ app.get() معاملين: مسار التوجيه (route path) والدالة التي سيتم استدعاؤها عندما يطلب العميل هذا المسار. لذا، يُترجم السطر الأخير إلى: يا خادم، استمع للطلبات على المسار ‘/’ (فكر في الصفحة الرئيسية) واستدعِ الدالة sayHi إذا تم تقديم طلب.
تمنحنا app.get أيضاً كائناً للطلب (request object) يحتوي على جميع البيانات المرسلة بواسطة العميل، وكائناً للاستجابة (response object) يحتوي على جميع الطرق التي يمكننا من خلالها الاستجابة للعميل. على الرغم من أن هذه الكائنات يمكن الوصول إليها كمعاملات دالة، إلا أن الاصطلاح العام للتسمية يقترح تسميتها res للاستجابة (response) و req للطلب (request).
كفى حديثاً. دعنا نشغل الخادم! قم بتشغيل الخادم التالي:
node server.js
إذا نجح كل شيء، يجب أن ترى رسالة في وحدة التحكم تقول:
Server is running on port 5000.
ملاحظة: يمكنك تغيير المنفذ إلى أي رقم تريده.

افتح متصفحك وانتقل إلى http://localhost:5000/ ويجب أن ترى شيئاً كهذا:

ها أنت ذا! أول طلب GET لك كان ناجحاً!
إرسال البيانات إلى خادم REST: طلب POST
كما ناقشنا سابقاً، دعنا نعد كيفية تنفيذ طلب POST في خادمنا. سنرسل رقمين وسيعيد الخادم مجموع الأرقام. أضف هذه الطريقة الجديدة أسفل app.get:
app.post("/add", (req, res) => {
const { a, b } = req.body;
res.send(`The sum is: ${a + b}`);
});
هنا، سنرسل البيانات بتنسيق JSON، هكذا:
{
"a": 5,
"b": 10
}
دعنا نراجع الكود:
- في السطر الأول، نستدعي طريقة
.post()منExpressJS، والتي تسمح للخادم بالاستماع لطلباتPOST. تأخذ هذه الدالة نفس المعاملات مثل طريقة.get(). المسار الذي نمرره هو/add، لذا يمكن الوصول إلى نقطة النهاية كـhttp://your-ip-address:port/addأو في حالتناlocalhost:5000/add. نحن ندمج دالتنا بدلاً من كتابة دالة في مكان آخر. - في السطر الثاني، استخدمنا جزءاً من بناء جملة
ES6، وهو تفكيك الكائنات (object destructuring). أي بيانات نرسلها عبر الطلب يتم تخزينها وتكون متاحة في خاصيةbodyمن كائنreq. لذا، كان بإمكاننا استبدال السطر الثاني بشيء مثل:
const num1 = req.body.a;
const num2 = req.body.b;
- في السطر الثالث، نستخدم دالة
send()من كائنresلإرسال نتيجة المجموع. مرة أخرى، نستخدم قوالب الحرفية (template literals) منES6.
الآن لاختبارها (باستخدام Postman):

لقد أرسلنا البيانات 5 و 10 كـ a و b باستخدامها كجسم للطلب (body). يقوم Postman بإرفاق هذه البيانات بالطلب وإرسالها. عندما يتلقى الخادم الطلب، يمكنه تحليل البيانات من req.body، كما فعلنا في الكود أعلاه. تظهر النتيجة أدناه.
إليك الكود النهائي لـ server.js حتى الآن:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.json());
const sayHi = (req, res) => {
res.send("Hi!");
};
app.get("/", sayHi);
app.post("/add", (req, res) => {
const { a, b } = req.body;
res.send(`The sum is: ${a + b}`);
});
app.listen(5000, () => {
console.log(`Server is running on port 5000.`);
});
بناء عميل REST: التفاعل مع الخادم من صفحة ويب
حسناً، لقد أنشأنا خادماً، ولكن كيف نصل إليه من موقعنا الإلكتروني أو تطبيق الويب الخاص بنا؟ هنا ستكون مكتبات عميل REST مفيدة. سنقوم بإنشاء صفحة ويب تحتوي على نموذج، حيث يمكنك إدخال رقمين وسنعرض النتيجة. لنبدأ.
أولاً، دعنا نغير ملف server.js قليلاً:
const path = require("path");
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.json());
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
});
app.post("/add", (req, res) => {
const { a, b } = req.body;
res.send({ result: parseInt(a) + parseInt(b) });
});
app.listen(5000, () => {
console.log(`Server is running on port 5000.`);
});
لقد قمنا باستيراد حزمة جديدة path، والتي يوفرها Node لمعالجة المسارات عبر الأنظمة الأساسية. بعد ذلك، قمنا بتغيير طلب GET على المسار ‘/’ واستخدمنا دالة أخرى متاحة في res، وهي sendFile، والتي تسمح لنا بإرسال أي نوع من الملفات كاستجابة. لذا، كلما حاول شخص ما الانتقال إلى ‘/’، سيحصل على صفحة index.html الخاصة بنا. أخيراً، قمنا بتغيير دالة app.post الخاصة بنا لإرجاع المجموع كـ JSON وتحويل كل من a و b إلى أعداد صحيحة باستخدام parseInt().
دعنا ننشئ صفحة HTML، سأسميها index.html، مع بعض التنسيقات الأساسية:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>REST Client</title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label, input[type="submit"] {
margin-top: 20px;
}
</style>
<body>
<div class="container">
<h1>Simple POST Form</h1>
<form>
<label>Number 1:</label>
<input id="num1" type="number" />
<label>Number 2:</label>
<input id="num2" type="number" />
<input type="submit" value="Add" />
</form>
<div class="result">Click Add!</div>
</div>
</body>
</html>
دعنا نضيف وسم script قبل وسم إغلاق body مباشرة، حتى لا نحتاج إلى الاحتفاظ بملف .js منفصل. سنبدأ بالاستماع لحدث submit واستدعاء دالة وفقاً لذلك:
<script>
document.addEventListener("submit", sendData);
</script>
أولاً، نحتاج إلى منع تحديث الصفحة عند النقر على زر ‘Add’. يمكن القيام بذلك باستخدام دالة preventDefault(). بعد ذلك، سنحصل على قيمة المدخلات في تلك اللحظة:
function sendData(e) {
e.preventDefault();
const a = document.querySelector("#num1").value;
const b = document.querySelector("#num2").value;
}
الآن سنجري الاتصال بالخادم بكلتا القيمتين a و b. سنستخدم واجهة Fetch API، المدمجة في كل متصفح لهذا الغرض. تأخذ Fetch مدخلين: نقطة نهاية URL وكائن طلب JSON، وتعيد وعداً (Promise). شرحها هنا سيكون خارج نطاق هذا الدليل، لذا سأترك ذلك لك. استمر داخل دالة sendData():
fetch("/add", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({ a: parseInt(a), b: parseInt(b) })
})
.then(res => res.json())
.then(data => {
const { result } = data;
document.querySelector(".result").innerText = `The sum is: ${result}`;
})
.catch(err => console.log(err));
أولاً، نمرر عنوان URL النسبي لنقطة النهاية كمعامل أول لـ fetch. بعد ذلك، نمرر كائناً يحتوي على الطريقة التي نريد أن تستخدمها Fetch للطلب، وهي POST في هذه الحالة. نمرر أيضاً headers، والتي ستوفر معلومات حول نوع البيانات التي نرسلها (content-type) ونوع البيانات التي نقبلها كاستجابة (accept).
بعد ذلك، نمرر body. هل تتذكر أننا كتبنا البيانات كـ JSON أثناء استخدام Postman؟ نحن نفعل شيئاً مشابهاً هنا. نظراً لأن Express يتعامل مع السلسلة (string) كمدخل ويعالجها وفقاً لـ content-type المقدم، نحتاج إلى تحويل حمولة JSON الخاصة بنا إلى سلسلة. نفعل ذلك باستخدام JSON.stringify(). نحن نتوخى الحذر قليلاً ونحلل المدخلات إلى أعداد صحيحة، حتى لا تفسد خادمنا (بما أننا لم ننفذ أي فحص لنوع البيانات).
أخيراً، إذا تم حل الوعد (promise) (الذي أعادته fetch)، فسنحصل على تلك الاستجابة ونحولها إلى JSON. بعد ذلك، سنحصل على النتيجة من المفتاح data الذي أعادته الاستجابة. ثم نعرض النتيجة ببساطة على الشاشة. في النهاية، إذا تم رفض الوعد، فسنعرض رسالة الخطأ في وحدة التحكم.
إليك الكود النهائي لـ index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>REST Client</title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label, input[type="submit"] {
margin-top: 20px;
}
</style>
<body>
<div class="container">
<h1>Simple POST Form</h1>
<form>
<label>Number 1:</label>
<input id="num1" type="number" />
<label>Number 2:</label>
<input id="num2" type="number" />
<input type="submit" value="Add" />
</form>
<div class="result">Click Add!</div>
</div>
<script>
document.addEventListener("submit", sendData);
function sendData(e) {
e.preventDefault();
const a = document.querySelector("#num1").value;
const b = document.querySelector("#num2").value;
fetch("/add", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({ a: parseInt(a), b: parseInt(b) })
})
.then(res => res.json())
.then(data => {
const { result } = data;
document.querySelector(".result").innerText = `The sum is: ${result}`;
})
.catch(err => console.log(err));
}
</script>
</body>
</html>
الخلاصة التقنية
في هذا المقال، استعرضنا بعمق بنية REST والمكونات الأساسية لطلبات REST. لقد قمنا ببناء خادم REST بسيط باستخدام Node.js و ExpressJS يتعامل مع طلبات GET و POST، ثم انتقلنا إلى إنشاء صفحة ويب تعمل كعميل REST. هذه الصفحة تستخدم Fetch API للتفاعل مع الخادم وعرض مجموع رقمين. هذا الدليل يمثل نقطة انطلاق ممتازة لفهم كيفية عمل واجهات برمجة التطبيقات في تطبيقات الويب الحديثة. يمكنك توسيع هذا المشروع ليشمل أنواع الطلبات المتبقية (PUT و DELETE) وحتى بناء تطبيق خلفي كامل يدعم عمليات CRUD (إنشاء، قراءة، تحديث، حذف).
إن فهم مبادئ REST أمر بالغ الأهمية لأي مطور ويب، حيث يشكل العمود الفقري لمعظم الاتصالات بين المكونات المختلفة للتطبيقات الموزعة. تذكر أن التصميم الجيد لواجهة API يساهم بشكل كبير في قابلية صيانة النظام وتوسعه ومرونته.