بناء واجهات REST مع Spring Boot

إصدار واجهة برمجة التطبيقات وأفضل الممارسات

18 دقيقة الدرس 9 من 13

إصدار واجهة برمجة التطبيقات وأفضل الممارسات

لقد بنيتَ بالفعل واجهة برمجة تطبيقات REST تعمل بكفاءة. الآن تأتي الحرفية الاحترافية: كيف تطوّر تلك الواجهة بمرور الوقت دون أن تُخلّ بعمل العملاء الحاليين؟ وكيف تُسمّيها وتهيكلها لتبقى قابلة للصيانة مع نموّها؟ يتناول هذا الدرس الاستراتيجيات الثلاث السائدة للإصدار، واتفاقيات التسمية التي تجعل الواجهة توثّق نفسها بنفسها، ونظرة موجزة على HATEOAS — القيد الذي يرفع REST إلى أنقى صوره.

لماذا الإصدار مهم

بمجرد أن يعتمد العملاء على واجهتك البرمجية، فإن أي تغيير قد يكسر التوافق — حذف حقل، إعادة تسميته، تغيير معنى رمز حالة — قد يُعطّلهم بصمت أو بضجيج. يمنحك الإصدار وسيلةً لإدخال تلك التغييرات على نسخة جديدة مع الإبقاء على النسخة القديمة حية حتى يكتمل انتقال العملاء. الإصدار عقد: كل ما في نطاق الإصدار الواحد يسلك سلوكًا ثابتًا.

ما الذي يُعدّ تغييرًا كاسرًا للتوافق؟ حذف حقل JSON أو إعادة تسميته، تغيير نوع بيانات حقل، حذف نقطة وصول، تغيير المعاملات المطلوبة في الطلب، وتعديل شكل استجابات الخطأ — كل ذلك يكسر التوافق. أما إضافة حقول اختيارية أو نقاط وصول جديدة فهي في الغالب آمنة.

الاستراتيجية الأولى — الإصدار في مسار URI

يُدرج رقم الإصدار مباشرةً في مسار العنوان: /api/v1/products، /api/v2/products. هذه أكثر الاستراتيجيات وضوحًا وانتشارًا في الواجهات العامة (تستخدمها Twitter وGitHub وStripe جميعها).

في Spring Boot لا تحتاج إلا إلى بادئة على @RequestMapping:

// المتحكم V1 — العقد الحالي @RestController @RequestMapping("/api/v1/products") public class ProductControllerV1 { @GetMapping("/{id}") public ResponseEntity<ProductDtoV1> getById(@PathVariable Long id) { // ... return ResponseEntity.ok(productService.findByIdV1(id)); } } // المتحكم V2 — شكل جديد (مثلًا: السعر أصبح كائن Money لا BigDecimal) @RestController @RequestMapping("/api/v2/products") public class ProductControllerV2 { @GetMapping("/{id}") public ResponseEntity<ProductDtoV2> getById(@PathVariable Long id) { return ResponseEntity.ok(productService.findByIdV2(id)); } }

يتشارك المتحكّمان طبقة الخدمة ذاتها؛ الفرق الوحيد هو في أشكال DTO وبادئة الرابط. ضع منطق الخدمة في مكان واحد — التكرار مسموح فقط في طبقة المتحكّم/DTO.

رتّب مشروعك حسب الإصدار: com.example.api.v1.controller، com.example.api.v2.controller. هذا يجعل الإصدارين قابلَين للتصفح جنبًا إلى جنب، ويُوضّح متى يصبح حذف إصدار آمنًا.

الاستراتيجية الثانية — الإصدار عبر ترويسة الطلب

يُمرَّر الإصدار في ترويسة HTTP مخصصة — شائعًا X-API-Version: 2 أو ترويسة Accept بنوع وسائط مخصص. يبقى الرابط نظيفًا (/api/products) ويمكن لنقطة وصول واحدة أن تتفرّع بحسب الترويسة، أو تستخدم خاصية headers في @GetMapping:

@RestController @RequestMapping("/api/products") public class ProductController { // يعالج: GET /api/products/{id} X-API-Version: 1 @GetMapping(value = "/{id}", headers = "X-API-Version=1") public ResponseEntity<ProductDtoV1> getByIdV1(@PathVariable Long id) { return ResponseEntity.ok(productService.findByIdV1(id)); } // يعالج: GET /api/products/{id} X-API-Version: 2 @GetMapping(value = "/{id}", headers = "X-API-Version=2") public ResponseEntity<ProductDtoV2> getByIdV2(@PathVariable Long id) { return ResponseEntity.ok(productService.findByIdV2(id)); } }

يحافظ الإصدار عبر الترويسة على نظافة الروابط، لكن له عيب حقيقي: الروابط لم تعد قابلة للإشارة المرجعية باستقلالية، ولا يمكن لشبكة CDN تخزينها مؤقتًا دون ضبط ترويسة Vary. كما أن الإصدار يصبح غير مرئي في شريط العنوان وفي السجلات ما لم تسجّل الترويسات صراحةً.

الاستراتيجية الثالثة — الإصدار عبر نوع الوسائط (ترويسة Accept)

هذا أكثر الأساليب انسجامًا مع REST: يُحدّد العميل التمثيل المطلوب عبر ترويسة Accept بنوع وسائط مخصص للمورّد، ويوجّه Spring إلى المعالج المناسب عبر produces:

@RestController @RequestMapping("/api/products") public class ProductController { @GetMapping( value = "/{id}", produces = "application/vnd.myapp.product.v1+json" ) public ResponseEntity<ProductDtoV1> getByIdV1(@PathVariable Long id) { return ResponseEntity.ok(productService.findByIdV1(id)); } @GetMapping( value = "/{id}", produces = "application/vnd.myapp.product.v2+json" ) public ResponseEntity<ProductDtoV2> getByIdV2(@PathVariable Long id) { return ResponseEntity.ok(productService.findByIdV2(id)); } }
الإصدار عبر نوع الوسائط أنيق نظريًا لكنه مؤلم عمليًا. معظم عملاء HTTP وبوابات API وأدوات المطورين ترسل افتراضيًا Accept: */*، لذا يجب توثيق نوع الوسائط المحدد باستمرار، واختباره صراحةً، والتحقق من أن البوابة لا تحذف ترويسات Accept المخصصة. كثير من الفرق تعود إلى إصدار URI بعد أن يتصاعد احتكاك استيعاب العملاء.

اختيار الاستراتيجية — نظرة سريعة على المقايضات

  • إصدار URI — عالي الوضوح، قابل للتخزين المؤقت بسهولة، سهل الاختبار في المتصفح. أقل "RESTية" قليلًا لأن الرابط يفترض أن يُعرّف موردًا لا إصدارًا. الاختيار الأمثل للواجهات العامة أو واسعة النطاق.
  • إصدار الترويسة — روابط نظيفة، غير مرئي لشبكات CDN بدون ضبط Vary، أصعب في الاختبار. جيد للواجهات الداخلية ذات العملاء المحكومين.
  • إصدار نوع الوسائط — أكثر توافقًا مع دلالات HTTP. احتكاك مرتفع لفرق المستهلكين؛ تجنّبه في الواجهات العامة.

لمعظم الفرق، إصدار URI هو الخيار العملي الافتراضي. GitHub وStripe وTwilio وأغلب الواجهات العامة الناضجة تستخدمه لهذا السبب تحديدًا.

اتفاقيات تسمية REST API

التسمية الجيدة تجعل الواجهة توثّق نفسها. هذه القواعد مُعتمَدة على نطاق واسع في الصناعة:

  • استخدم الأسماء لا الأفعال/products لا /getProducts. فعل HTTP هو الذي يوفّر الفعل.
  • صيغة الجمع لأسماء المجموعات/users، /orders. المجموعة مجموعة موارد.
  • أحرف صغيرة مفصولة بشرطة/product-categories لا productCategories ولا product_categories. الروابط حساسة لحالة الأحرف على بعض الخوادم؛ الأحرف الصغيرة تزيل الغموض.
  • التسلسل الهرمي يعكس العلاقات/users/{userId}/orders/{orderId}. ادمج الموارد الفرعية تحت أصلها، لكن لا تتجاوز مستويَين أو ثلاثة وإلا أصبحت الروابط مرهقة.
  • التصفية والترتيب والتصفح عبر معاملات الاستعلام/products?category=electronics&sort=price&page=2&size=20. أبقِ المسار نظيفًا؛ المسار هو هوية المورد، ومعاملات الاستعلام قيود اختيارية.
  • استخدم رموز حالة HTTP القياسية باتساق201 Created مع ترويسة Location بعد POST، و204 No Content عند نجاح DELETE، و404 Not Found عندما لا يوجد المورد (لا 200 مع جسم خطأ).
وثّق شكل الخطأ وتمسّك به. جسم خطأ موحّد — مثلًا { "status": 404, "error": "Not Found", "message": "Product 42 not found", "timestamp": "..." } — يتيح للعملاء كتابة معالج خطأ واحد لكامل واجهتك بدلًا من تخمين الشكل لكل نقطة وصول.

ملاحظة حول HATEOAS

يتضمن REST بحسب تعريف Roy Fielding قيدًا يُسمّى HATEOAS (Hypermedia As The Engine Of Application State). في واجهة HATEOAS، تتضمن كل استجابة روابط تُخبر العميل بما يمكنه فعله بعد ذلك — فلا يحتاج العميل إلى تضمين الروابط مسبقًا، بل يكتشفها في وقت التشغيل:

// استجابة HATEOAS لـ GET /api/v1/orders/99 { "id": 99, "status": "PENDING", "total": 149.99, "_links": { "self": { "href": "/api/v1/orders/99" }, "confirm": { "href": "/api/v1/orders/99/confirm", "method": "POST" }, "cancel": { "href": "/api/v1/orders/99/cancel", "method": "DELETE" }, "customer":{ "href": "/api/v1/customers/7" } } }

يوفّر Spring مكتبة Spring HATEOAS (المُبدِّل: spring-boot-starter-hateoas) مع EntityModel وCollectionModel وWebMvcLinkBuilder لبناء هياكل الروابط هذه دون إنشاء سلاسل نصية يدويًا.

import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; @GetMapping("/{id}") public EntityModel<OrderDto> getOrder(@PathVariable Long id) { OrderDto order = orderService.findById(id); return EntityModel.of(order, WebMvcLinkBuilder.linkTo( WebMvcLinkBuilder.methodOn(OrderController.class).getOrder(id) ).withSelfRel(), WebMvcLinkBuilder.linkTo( WebMvcLinkBuilder.methodOn(OrderController.class).confirmOrder(id) ).withRel("confirm") ); }

يجعل HATEOAS الواجهة تصف نفسها بنفسها حقًا ويفصل العملاء عن هياكل الروابط المضمّنة. في الواقع العملي، تتجاهله معظم الفرق التي تبني واجهات خاصة أو موجّهة لشركاء محددين لأن التكلفة التشغيلية كبيرة وأغلب العملاء لا يتتبعون الروابط ديناميكيًا. قيمته الأكبر في الواجهات العامة ذات المستهلكين المتنوعين. معرفة وجوده — وإدراك نمط _links حين تصادفه — هو ما يهم هنا.

الخلاصة

إصدار مسار URI (/api/v1/) هو الاختيار العملي المعياري في الصناعة لمعظم الواجهات. استراتيجيتا الترويسة ونوع الوسائط تستبدلان وضوح الرابط بنقاء HTTP على حساب تعقيد العميل. اتفاقيات التسمية الصلبة — أسماء جمع، مسارات بأحرف صغيرة وشرطات، أفعال من HTTP، معاملات استعلام للتصفية — تحوّل الواجهة العاملة إلى واجهة مُصمَّمة جيدًا. HATEOAS هو المثل الأعلى لـ REST يستحق الفهم حتى لو لم تطبّقه من اليوم الأول. في الدرس الأخير من هذا البرنامج التعليمي ستجمع كل هذه المهارات معًا لبناء واجهة REST كاملة بجودة الإنتاج من الصفر.