توحيد معايير واجهات RESTful JSON API باستخدام مواصفة OpenAPI
منذ زمن بعيد، وتطوير واجهات JSON API ساحة مفتوحة بلا قواعد واضحة. صار بإمكاننا بناء واجهات معقدة بسرعة، لكن كل واجهة تتبع عادةً معاييرها الخاصة. كانت هناك بعض القواعد غير المكتوبة وتبادل للأفكار، لكن لم توجد من قبل مواصفة متكاملة بهذا الشكل.

ما هي مواصفة JSON API؟
لفترة طويلة جدًا، كانت واجهات JSON API تُبنى بهياكل مختلفة تمامًا. فقد تبدو واجهة ما مختلفة كليًا عن الأخرى، وأحيانًا تتشابه في أمور مثل استخدام /api/v1 لتحديد الإصدار، أو هيكلة الـ params بحيث يكون الـ model هو العقدة الجذرية، مثل { base_node: { foo: "bar" } }، لكن التشابه كان يتوقف عند هذا الحد في الغالب. فكل مشروع، وأحيانًا كل فريق، يضع معاييره الخاصة. وفي أسوأ الحالات، يكون لكل endpoint تنسيقه الخاص. هذا التشتت مكلف، أما مع وجود مواصفة موحدة فنعرف ما نتوقعه من كل endpoint في أي تطبيق. وهذا يتيح لنا بناء معرفة قابلة للنقل بين المشاريع، ومكتبات توفّر الوقت وتقلّل الأخطاء.
في عام 2013 قرر Yehuda Katz أن يجرّب صياغة مواصفة لواجهات JSON API. وقد استُخلصت المسودة الأولى من طريقة نقل JSON التي كان يعرّفها ضمنيًا الـ REST adapter في Ember Data. ووصلت المواصفة إلى الإصدار المستقر 1.0 في مايو 2015، ومنذ ذلك الحين يتناقشها مساهمون كثيرون ويتواصل تطويرها بوتيرة ثابتة. ونأمل أن تصل المواصفة قريبًا إلى الإصدار v1.1، ويمكنكم الاطلاع على التغييرات هنا https://jsonapi.org/#update-history
البداية
يجب إرسال جميع الطلبات مع الترويستين Content-Type و_Accepts_ بنوع MIME التالي: application/vnd.api+json ولإنشاء مورد (resource) جديد، نرسل طلب POST يتضمن مستند json بالتنسيق التالي.
{
"data": {
"type": "foo",
"attributes": {
"title": "Foo resource",
"url": "https://example.com/bar.html"
},
"relationships": {
"bar": {
"data": {
"type": "bar",
"id": "9"
}
}
}
}
}
كما ترون، يتضمن المستند عقدة data في الجذر تعرّف كائن مورد واحدًا. ويحدّد كائن المورد النوع (type) والسمات (attributes) والعلاقات (relationships). ويمكننا بعد ذلك استخدام هذه المعلومات في الـ backend لإنشاء الكائن الصحيح في قاعدة البيانات. وعند إنشاء مورد، يمكن للعميل تقديم UUID في الحقل data.attributes.id لتحديد المعرّف (ID) الخاص بالمورد الجديد. حاليًا يُسمح بعملية واحدة فقط في كل طلب، لكن هناك نقاشات حول طريقة دعم عمليات متعددة في الطلب الواحد مستقبلًا، ما سيتيح لنا إنشاء العلاقات في الوقت نفسه الذي ننشئ فيه المورد. وسيُضاف ذلك في الإصدارات القادمة من المواصفة. وتحدّد المواصفة أيضًا بعض القواعد حول كيفية تنسيق الاستجابات لطلباتنا. إذا أُنشئ المورد بنجاح وكان العميل قد قدّم معرّفًا، يمكننا إرجاع استجابة 204 No Content، وهذا يقلّل حجم البيانات المتبادلة. أما إذا أُنشئ المورد بنجاح دون تقديم معرّف، فيجب أن نرجع 201 مع ترويسة Location ومستند يحتوي على المورد الذي أُنشئ للتو، مثل:
HTTP/1.1 201 Created
Location: https://example.com/foos/ddf358c2-a0fb-4035-946d-4b8f96ea334b
Content-Type: application/vnd.api+json
{
"data": {
"type": "foo",
"id": "ddf358c2-a0fb-4035-946d-4b8f96ea334b",
"attributes": {
"title": "Foo resource",
"url": "https://example.com/bar.html"
},
"links": {
"self": "https://example.com/foos/ddf358c2-a0fb-4035-946d-4b8f96ea334b"
}
}
}
كما ترون، إنشاء الموارد أمر بسيط نسبيًا. وبما أنه أصبح موحدًا، يمكننا كتابة كود جاهز (boilerplate) نعيد استخدامه في جميع تطبيقاتنا. لكن هذا ليس المجال الذي تتألق فيه مواصفة JSON API حقًا، فالتحسينات الحقيقية تظهر عند جلب البيانات!
جلب الموارد والعلاقات
الفرق الوحيد بين استجابة المورد الفردي واستجابة المجموعة هو محتوى عقدة data. فالـ endpoints الخاصة بالمجموعات ترجع مصفوفة من كائنات الموارد، بينما ترجع الـ endpoint الفردية كائن مورد واحدًا، مثل:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
}
}
}
مقابل
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
},
"relationships": {
"author": {
"links": {
"related": "https://example.com/articles/1/author"
}
}
}
}, {
"type": "articles",
"id": "2",
"attributes": {
"title": "Rails is Omakase"
},
"relationships": {
"author": {
"links": {
"related": "https://example.com/articles/1/author"
}
}
}
}]
}
كما ترون في مثال المجموعة، كتب المقالتين المؤلف نفسه. ويتوفر رابط لجلب بيانات المؤلف، ما يتيح لنا تحميلها وتخزينها في الـ cache مرة واحدة فقط لدى العميل. وبدلًا من ذلك، يمكننا أن نطلب من الـ backend تضمين علاقة المؤلف في الاستجابة.
تضمين العلاقات
لتضمين علاقة، نحتاج إلى تمرير قائمة العلاقات التي نريد إرجاعها في الـ query params عند إرسال الطلب، مثل:
/articles/1?include=authors
سيُرجع هذا العلاقة بتنسيق مختلف قليلًا:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "Lorem ipsum",
"published-at": "2020-20-01T00:00:00+7:00"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
}
}
},
"included": [{
"type": "people",
"id": "9",
"attributes": {
"first-name": "Dan",
"last-name": "Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "http://example.com/people/9"
}
}]
}
قد يبدو هذا مبالغًا فيه مع مورد واحد، لكن عندما ترجعون 30 سجلًا تتشارك الكثير من البيانات، فإنه يقلّل حجم البيانات المتبادلة بشكل ملحوظ. كما يمنح هذا الأسلوب العميل تحكمًا أكبر بكثير في تحديد ما يريده بالضبط.
السماح للعملاء بطلب جزء من الحقول عبر sparse fieldsets
تعرّف مواصفة JSON API أيضًا آلية تتيح للعميل تقليل عدد الحقول التي تُرجع لكل مورد. ولطلب sparse fieldset، يمكننا تمرير الحقول عبر الـ query parameters مرة أخرى، بل يمكننا تمريرها للعلاقات أيضًا:
/articles?include=author&fields[articles]=title,body&fields[people]=name
سيُرجع هذا الطلب ما يلي:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "Lorem ipsum",
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/articles/1/relationships/author",
"related": "http://example.com/articles/1/author"
},
"data": { "type": "people", "id": "9" }
}
}
},
"included": [{
"type": "people",
"id": "9",
"attributes": {
"name": "D. Gebhardt"
},
"links": {
"self": "http://example.com/people/9"
}
}]
}
الخلاصة
لم يتناول هذا المقال إلا القليل مما تعرّفه المواصفة. وهي تستحق فعلًا أن تطّلعوا عليها، فهي تغطي ترقيم الصفحات (pagination)، وتنسيق الأخطاء، وأنواع الاستجابات، وإنشاء الموارد وتحديثها، وترتيب النتائج، وغير ذلك الكثير. يمكنكم قراءة المواصفة كاملة على الموقع الرسمي https://jsonapi.org/ تتبنى شركات أكثر فأكثر مواصفة JSON API. فقد طوّرت Netflix مكتبة fast_jsonapi وأتاحتها لمجتمع المصادر المفتوحة. وهناك أيضًا gems أخرى كثيرة، فمكتبة active_model_serializers تدعم الآن مواصفة JSON API. وآمل أن تتبنى شركات أكثر هذه المواصفة لنجعلها معيارًا عامًا. ويبدو أن Rails يتجه نحو اعتمادها افتراضيًا في وضع API. المستقبل يبدو مبشّرًا جدًا.


