تطوير واجهات REST API

أنماط بوابة API

15 دقيقة الدرس 26 من 35

فهم أنماط بوابة API

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

ما هي بوابة API؟

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

مفهوم أساسي: نمط بوابة API ينفذ نمط "الواجهة الخلفية للواجهة الأمامية" (BFF)، حيث يمكن للعملاء المختلفين (الويب، الموبايل، إنترنت الأشياء) أن يكون لديهم تنفيذات بوابة مخصصة مصممة لاحتياجاتهم المحددة.

المسؤوليات الأساسية لبوابة API

1. توجيه الطلبات

تقوم البوابة بتعيين الطلبات الواردة إلى خدمات الخلفية المناسبة بناءً على أنماط URL أو الرؤوس أو معايير أخرى.

# تكوين توجيه Nginx location /api/users/ { proxy_pass http://user-service:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/products/ { proxy_pass http://product-service:3001/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/orders/ { proxy_pass http://order-service:3002/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

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

مركزية منطق المصادقة على مستوى البوابة لتجنب تكرار كود الأمان عبر الخدمات.

// مصادقة بوابة API في Express.js const express = require('express'); const jwt = require('jsonwebtoken'); const app = express(); // middleware المصادقة const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'لم يتم توفير رمز' }); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ error: 'رمز غير صالح' }); } req.user = user; next(); }); }; // تطبيق المصادقة على جميع مسارات /api app.use('/api', authenticateToken); // مسار إلى خدمة المستخدم app.use('/api/users', createProxyMiddleware({ target: 'http://user-service:3000', changeOrigin: true, pathRewrite: { '^/api/users': '/' }, onProxyReq: (proxyReq, req) => { // إعادة توجيه معلومات المستخدم إلى خدمات الخلفية proxyReq.setHeader('X-User-Id', req.user.id); proxyReq.setHeader('X-User-Role', req.user.role); } }));
أفضل ممارسة: استخدم رموز JWT مع أوقات انتهاء قصيرة ونفذ آليات تحديث الرمز. قم بتخزين العمليات الحساسة (مثل أدوار المستخدم) في حمولة الرمز لتجنب عمليات بحث إضافية في قاعدة البيانات.

استراتيجيات موازنة التحميل

توزع بوابات API الطلبات الواردة عبر نسخ متعددة من خدمات الخلفية لضمان التوفر العالي والأداء الأمثل.

خوارزميات موازنة التحميل الشائعة

1. Round Robin: توزع الطلبات بشكل متسلسل عبر الخوادم المتاحة.

# موازنة التحميل round-robin في Nginx upstream user_service { server user-service-1:3000; server user-service-2:3000; server user-service-3:3000; } location /api/users/ { proxy_pass http://user_service/; }

2. الاتصالات الأقل: توجه الطلبات إلى الخادم الذي لديه أقل عدد من الاتصالات النشطة.

upstream user_service { least_conn; server user-service-1:3000; server user-service-2:3000; server user-service-3:3000; }

3. IP Hash: يضمن أن نفس العميل يصل دائمًا إلى نفس الخادم (مفيد للجلسات الثابتة).

upstream user_service { ip_hash; server user-service-1:3000; server user-service-2:3000; server user-service-3:3000; }

4. موازنة التحميل الموزونة: تخصص المزيد من الحركة للخوادم القوية.

upstream user_service { server user-service-1:3000 weight=3; # يحصل على 60% من الحركة server user-service-2:3000 weight=2; # يحصل على 40% من الحركة }
فحوصات الصحة: قم دائمًا بتنفيذ فحوصات الصحة لإزالة الخوادم غير الصحية تلقائيًا من مجموعة موازنة التحميل. يمكن لـ Nginx إجراء فحوصات صحة سلبية، بينما يوفر Nginx Plus فحوصات صحة نشطة.

تحويل الطلبات والاستجابات

يمكن للبوابات تعديل الطلبات قبل إعادة توجيهها وتحويل الاستجابات قبل الإرجاع إلى العملاء.

// تحويل الطلب/الاستجابة في Express.js const axios = require('axios'); app.get('/api/products/:id', authenticateToken, async (req, res) => { try { // جلب من خدمات متعددة const [product, reviews, inventory] = await Promise.all([ axios.get(`http://product-service:3001/products/${req.params.id}`), axios.get(`http://review-service:3002/reviews?productId=${req.params.id}`), axios.get(`http://inventory-service:3003/inventory/${req.params.id}`) ]); // تجميع وتحويل الاستجابة const aggregatedResponse = { id: product.data.id, name: product.data.name, price: product.data.price, description: product.data.description, averageRating: calculateAverageRating(reviews.data), reviewCount: reviews.data.length, inStock: inventory.data.quantity > 0, stockQuantity: inventory.data.quantity, // إضافة حقول محسوبة isOnSale: product.data.salePrice < product.data.price, savings: product.data.price - product.data.salePrice }; res.json(aggregatedResponse); } catch (error) { console.error('خطأ في البوابة:', error); res.status(500).json({ error: 'فشل في جلب تفاصيل المنتج' }); } }); function calculateAverageRating(reviews) { if (reviews.length === 0) return 0; const sum = reviews.reduce((acc, review) => acc + review.rating, 0); return (sum / reviews.length).toFixed(1); }

الحد من المعدل والتحكم

حماية خدمات الخلفية من الحمل الزائد من خلال تنفيذ الحد من المعدل على مستوى البوابة.

// الحد من المعدل في Express مع redis const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redis = new Redis({ host: 'localhost', port: 6379 }); // إنشاء محدد المعدل const apiLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: 'rate_limit:', }), windowMs: 15 * 60 * 1000, // 15 دقيقة max: 100, // حد كل IP إلى 100 طلب لكل نافذة زمنية message: { error: 'طلبات كثيرة جدًا من هذا IP، يرجى المحاولة لاحقًا.' }, standardHeaders: true, // إرجاع معلومات حد المعدل في الرؤوس legacyHeaders: false, }); // تطبيق الحد من المعدل على مسارات API app.use('/api/', apiLimiter); // حدود مختلفة لنقاط النهاية المختلفة const strictLimiter = rateLimit({ store: new RedisStore({ client: redis }), windowMs: 15 * 60 * 1000, max: 10, // حد أكثر صرامة للنقاط الحساسة }); app.use('/api/auth/login', strictLimiter); app.use('/api/auth/register', strictLimiter);
# الحد من المعدل في Nginx # تعريف منطقة حد المعدل (10MB يمكن أن يخزن ~160,000 عنوان IP) limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; location /api/ { # تطبيق حد المعدل (السماح بدفعات من 20 طلب) limit_req zone=api_limit burst=20 nodelay; # استجابة خطأ مخصصة limit_req_status 429; proxy_pass http://backend_services; }

حلول بوابة API الشائعة

Kong API Gateway

Kong هي بوابة API سحابية أصلية ومستقلة عن المنصة مبنية على Nginx. توفر نظامًا بيئيًا واسعًا للمكونات الإضافية وميزات المؤسسة.

# مثال تكوين Kong (تكوين تصريحي) _format_version: "2.1" services: - name: user-service url: http://user-service:3000 routes: - name: user-routes paths: - /api/users plugins: - name: rate-limiting config: minute: 100 policy: local - name: jwt config: secret_is_base64: false - name: cors config: origins: - "*" methods: - GET - POST - PUT - DELETE headers: - Authorization - Content-Type - name: product-service url: http://product-service:3001 routes: - name: product-routes paths: - /api/products plugins: - name: response-transformer config: add: headers: - X-Service:product-service
مزايا Kong: بنية المكونات الإضافية، المصادقة/التفويض المدمج، التكوين التصريحي، قدرات شبكة الخدمة، والأداء الممتاز. إنه مناسب بشكل خاص لبنى الخدمات المصغرة.

AWS API Gateway

خدمة مُدارة بالكامل تجعل من السهل إنشاء ونشر وصيانة ومراقبة وتأمين واجهات برمجة التطبيقات على أي مقياس.

// AWS API Gateway مع تكامل Lambda (Serverless Framework) service: api-gateway-example provider: name: aws runtime: nodejs18.x region: us-east-1 functions: getUsers: handler: handlers/users.get events: - http: path: users method: get cors: true authorizer: name: authFunction resultTtlInSeconds: 300 createUser: handler: handlers/users.create events: - http: path: users method: post cors: true authorizer: name: authFunction authFunction: handler: handlers/auth.authorize resources: Resources: ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: Name: MyAPI # إعدادات التحكم في API ApiGatewayUsagePlan: Type: AWS::ApiGateway::UsagePlan Properties: UsagePlanName: BasicPlan Throttle: BurstLimit: 200 RateLimit: 100

نمط قاطع الدائرة مع بوابة API

منع الفشل المتتالي من خلال تنفيذ قواطع الدائرة على مستوى البوابة.

// تنفيذ قاطع الدائرة باستخدام opossum const CircuitBreaker = require('opossum'); const axios = require('axios'); // دالة لاستدعاء خدمة الخلفية async function callUserService(userId) { const response = await axios.get(`http://user-service:3000/users/${userId}`); return response.data; } // خيارات قاطع الدائرة const options = { timeout: 3000, // إذا استغرقت الدالة أكثر من 3 ثوانٍ، أطلق الفشل errorThresholdPercentage: 50, // عندما تفشل 50% من الطلبات، افتح الدائرة resetTimeout: 10000, // بعد 10 ثوانٍ، حاول مرة أخرى (حالة نصف مفتوحة) volumeThreshold: 10, // الحد الأدنى لعدد الطلبات قبل التعثر }; // إنشاء قاطع الدائرة const breaker = new CircuitBreaker(callUserService, options); // معالجة أحداث الدائرة breaker.on('open', () => { console.log('قاطع الدائرة مفتوح - الخدمة معطلة'); }); breaker.on('halfOpen', () => { console.log('قاطع الدائرة نصف مفتوح - اختبار الخدمة'); }); breaker.on('close', () => { console.log('قاطع الدائرة مغلق - الخدمة صحية'); }); // استخدام في مسار Express app.get('/api/users/:id', async (req, res) => { try { const user = await breaker.fire(req.params.id); res.json(user); } catch (error) { if (breaker.opened) { // الدائرة مفتوحة، أعد البيانات المخزنة مؤقتًا أو البديل res.status(503).json({ error: 'الخدمة غير متوفرة مؤقتًا', cached: true }); } else { res.status(500).json({ error: 'خطأ داخلي في الخادم' }); } } });
تحذير: يجب تكوين قواطع الدائرة بعناية. تحديد عتبات منخفضة جدًا يمكن أن يسبب نتائج إيجابية خاطئة، في حين أن تحديدها مرتفعة جدًا يهزم الغرض من وجود قاطع دائرة.

تجميع وتكوين الطلبات

يمكن لبوابات API دمج مكالمات خلفية متعددة في طلب عميل واحد، مما يقلل من التحميل على الشبكة.

// التجميع على طراز GraphQL في بوابة REST API app.get('/api/dashboard/:userId', authenticateToken, async (req, res) => { const { userId } = req.params; try { // طلبات متوازية إلى خدمات متعددة const [user, orders, recommendations, notifications] = await Promise.allSettled([ axios.get(`http://user-service:3000/users/${userId}`), axios.get(`http://order-service:3001/orders?userId=${userId}&limit=5`), axios.get(`http://recommendation-service:3002/recommendations/${userId}`), axios.get(`http://notification-service:3003/notifications/${userId}?unread=true`) ]); // بناء الاستجابة المجمعة const dashboard = { user: user.status === 'fulfilled' ? user.value.data : null, recentOrders: orders.status === 'fulfilled' ? orders.value.data : [], recommendations: recommendations.status === 'fulfilled' ? recommendations.value.data : [], unreadNotifications: notifications.status === 'fulfilled' ? notifications.value.data : [], // إضافة البيانات الوصفية loadedAt: new Date().toISOString(), partialFailure: [user, orders, recommendations, notifications].some(r => r.status === 'rejected') }; res.json(dashboard); } catch (error) { console.error('خطأ في تجميع لوحة القيادة:', error); res.status(500).json({ error: 'فشل في تحميل لوحة القيادة' }); } });

التخزين المؤقت على مستوى البوابة

تنفيذ التخزين المؤقت للاستجابة لتقليل الحمل على الخلفية وتحسين الأداء.

// التخزين المؤقت للاستجابة المستند إلى Redis const redis = require('redis'); const client = redis.createClient({ url: 'redis://localhost:6379' }); client.on('error', err => console.error('خطأ Redis:', err)); await client.connect(); // middleware التخزين المؤقت const cacheMiddleware = (duration) => { return async (req, res, next) => { const key = `cache:${req.originalUrl}`; try { // فحص ذاكرة التخزين المؤقت const cachedResponse = await client.get(key); if (cachedResponse) { console.log('إصابة ذاكرة التخزين المؤقت:', key); return res.json(JSON.parse(cachedResponse)); } // تجاوز res.json لتخزين الاستجابة مؤقتًا const originalJson = res.json.bind(res); res.json = (data) => { // تخزين الاستجابة مؤقتًا client.setEx(key, duration, JSON.stringify(data)) .catch(err => console.error('خطأ في تعيين ذاكرة التخزين المؤقت:', err)); return originalJson(data); }; next(); } catch (error) { console.error('خطأ في middleware التخزين المؤقت:', error); next(); // المتابعة بدون تخزين مؤقت عند حدوث خطأ } }; }; // تطبيق التخزين المؤقت على مسارات محددة app.get('/api/products', cacheMiddleware(300), async (req, res) => { const response = await axios.get('http://product-service:3001/products'); res.json(response.data); });
تمرين: صمم بوابة API لمنصة تجارة إلكترونية مع المتطلبات التالية:
  1. ثلاث خدمات خلفية: خدمة المستخدم (المنفذ 3000)، خدمة المنتج (المنفذ 3001)، خدمة الطلب (المنفذ 3002)
  2. تنفيذ مصادقة JWT لجميع المسارات
  3. تطبيق الحد من المعدل: 100 طلب لكل 15 دقيقة للنقاط العامة، 10 طلبات لكل 15 دقيقة لنقاط المصادقة
  4. إنشاء نقطة نهاية مجمعة /api/checkout/:orderId تجلب تفاصيل الطلب ومعلومات المستخدم ومعلومات المنتج في طلب واحد
  5. تنفيذ التخزين المؤقت للاستجابة لقوائم المنتجات (5 دقائق TTL)
  6. إضافة نقاط نهاية فحص الصحة لكل خدمة خلفية

اختر إما Nginx أو Kong أو Express.js وقدم التكوين الكامل.