Get a quote

تصميم REST API احترافي لأنظمة SaaS باستخدام Go

تصميم API صحيح يقلل من وقت التطوير، يسهل التكامل مع العملاء، ويجعل التوسع أسهل. هذه المعايير التي نتبعها في Voxire لبناء APIs إنتاجية لأنظمة SaaS متعددة المستأجرين.

لماذا تصميم API غير المتسق يكلّف الفرق وقتاً طويلاً

تصميم API غير المتسق هو أحد أكثر المشكلات الهندسية تكلفةً من الناحية التقنية عند الإصلاح بعد النشر. كل اصطلاح يبدو بسيطاً في البداية: أسماء حقول متضاربة، تنسيقات أخطاء غير متسقة، إصدار عشوائي، يخلق ألماً متراكماً مع نمو عدد العملاء.

في Voxire بنينا APIs بلغة Go لـ RTYLR وعدد من الأنظمة للعملاء في لبنان والخليج. المعايير في هذا المقال هي ما توصلنا إليه بعد تكرار المرور بعدم الاتساق الذي كلّف وقتاً هندسياً حقيقياً.

بنية URL الصحيحة والموارد المتداخلة

URLs تُعرّف الموارد. يجب أن تُقرأ كأسماء، لا أفعال. اسم المورد يجب أن يكون الصيغة الجمع للكيان.

الصحيح:

GET    /api/v1/tenants/{tenant_id}/locations
POST   /api/v1/tenants/{tenant_id}/locations
GET    /api/v1/tenants/{tenant_id}/locations/{location_id}
PATCH  /api/v1/tenants/{tenant_id}/locations/{location_id}

الخطأ:

GET  /api/v1/getLocations
POST /api/v1/createLocation
GET  /api/v1/location?id=123

عمق التداخل يجب أن يعكس الملكية الفعلية. location تنتمي إلى tenant. order تنتمي إلى location. التداخل بأكثر من مستويين يصبح محرجاً.

الإصدار الصحيح لـ API

جميع APIs المبنية في Voxire تستخدم إصدار مسار URL: /api/v1/. الإصدار عبر الـ header يعمل بشكل جيد لكنه يخلق احتكاكاً للعملاء الذين يستخدمون أدوات HTTP الأساسية. إصدار URL مرئي، قابل للتخزين المؤقت، وقابل للاختبار بدون أدوات خاصة.

قاعدة رفع الإصدار: أي تغيير كاسر يتطلب نسخة جديدة. التغييرات الكاسرة هي:

  • حذف حقل من استجابة
  • تغيير نوع حقل
  • تغيير اسم حقل
  • حذف نقطة نهاية

في حالة RTYLR، تطبيقات الجوال وأجهزة POS لا يمكن تحديثها في وقت واحد، لذا نوافذ دعم الإصدار لا تقل عن ستة أشهر كمعيار.

نمط الاستجابة الموحد

كل استجابة API تستخدم نفس الغلاف JSON:

type Response[T any] struct {
    Data  T           `json:"data"`
    Meta  *Meta       `json:"meta,omitempty"`
    Error *ErrorBody  `json:"error,omitempty"`
}

type ErrorBody struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details any    `json:"details,omitempty"`
}

استجابة القائمة الناجحة:

{
  "data": [{ "id": "...", "name": "..." }],
  "meta": { "page": 1, "page_size": 25, "total": 143 }
}

استجابة الخطأ:

{
  "data": null,
  "error": {
    "code": "LOCATION_NOT_FOUND",
    "message": "لا يوجد موقع بهذا المعرف ضمن حسابك."
  }
}

حقل data موجود دائماً (null عند الأخطاء). حقل error موجود دائماً (null عند النجاح).

ترقيم الصفحات: cursor vs offset

RTYLR يستخدم نمطَي ترقيم حسب حالة الاستخدام.

ترقيم بالإزاحة للقوائم المحدودة حيث العدد الإجمالي مهم:

GET /api/v1/tenants/{id}/locations?page=2&page_size=25

ترقيم بالمؤشر للبيانات الزمنية والمجموعات الكبيرة:

GET /api/v1/tenants/{id}/orders?cursor=eyJpZCI6IjEyMyJ9&limit=50

المؤشر يُشفّر موضع آخر سجل تم رؤيته. هذا مناسب لسجل الطلبات والتدقيق وأي قائمة يُمرّر فيها تطبيق الجوال بشكل تدريجي.

معالجة الأخطاء بشكل احترافي

var (
    ErrLocationNotFound = &APIError{Code: "LOCATION_NOT_FOUND", Status: 404}
    ErrOrderAlreadyVoid = &APIError{Code: "ORDER_ALREADY_VOIDED", Status: 409}
    ErrInvalidInput     = &APIError{Code: "INVALID_INPUT", Status: 422}
    ErrUnauthorized     = &APIError{Code: "UNAUTHORIZED", Status: 401}
)

القاعدة: لا تُرجع أبداً 200 مع خطأ في الجسم، ولا تُرجع أبداً 500 مع تفاصيل stack trace في الإنتاج. الأخطاء الداخلية تُسجَّل بالـ stack trace الكامل لكن العميل يستقبل فقط رسالة عامة.

المصادقة والتفويض في header

RTYLR يستخدم Bearer tokens في header المصادقة. الـ middleware يستخرج الـ token، يتحقق من JWT، ويُدرج قيمة context للطلب:

type RequestContext struct {
    TenantID uuid.UUID
    UserID   uuid.UUID
    Role     string
}

دوال المعالج تسترد معرف المستأجر من الـ context، لا من معاملات URL أو جسم الطلب. معالج يبني استعلاماً باستخدام tenant_id من جسم الطلب بدلاً من مطالبات JWT هو خلل أمني.

توثيق API بـ OpenAPI/Swagger في Go

توثيق RTYLR يُولَّد من tags الـ struct باستخدام swaggo:

// GetLocations returns all locations for a tenant.
// @Summary List locations
// @Tags locations
// @Produce json
// @Success 200 {object} Response[[]Location]
// @Failure 401 {object} Response[any]
// @Router /api/v1/tenants/{tenant_id}/locations [get]
func (h *LocationHandler) GetLocations(w http.ResponseWriter, r *http.Request) {
    // implementation
}

خط أنابيب CI يشغّل swag init ويفشل إذا كانت المواصفة المُولَّدة تحتوي على تغييرات غير مُودَعة، مما يُطبّق بقاء الوثائق حديثة.

خبرة من Voxire في بناء APIs لـ RTYLR

بناء وصيانة API RTYLR عبر عملاء في لبنان وأسواق الخليج أفرز أنماطاً تستحق التوثيق.

مسارات URL متعددة المستأجرين لها مقايضة. تضمين tenant_id في مسار URL صريح وسهل التوجيه، لكنه يعني أن كل عميل يجب أن يعرف معرف مستأجره قبل أي استدعاء API. RTYLR يستخدم المسار الصريح لأن أجهزة POS مشتركة عبر سياقات مستأجرين متعددة في الامتيازات، ووجود معرف المستأجر في المسار يجعل سجلات الطلبات أسهل قراءة.

اختبر API بعميل HTTP حقيقي. اختبارات التكامل التي تُشغّل خادم HTTP الفعلي وتُجري طلبات حقيقية تكتشف أخطاء التوجيه والـ middleware والتسلسل التي تُفوّتها اختبارات الوحدة.

هل تبني API بـ Go لمنصة SaaS وتريد رأياً ثانياً في البنية؟ فريق هندسة Voxire متاح لمساعدتك. ابدأ المحادثة على https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp