Kodokon kodokon.com

هندسة واجهة برمجة تطبيقات (API): المسارات والمتحكمات والخدمات

هيكِل واجهة برمجة التطبيقات (API) في طبقات ذات مسؤولية واحدة، واربطها معًا بحقن خفيف لجعلها قابلة للاختبار.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

إن الملف الواحد الذي يخلط بين التوجيه ومنطق الأعمال والوصول إلى البيانات يصبح مكلفًا دائمًا: فلا يمكنك اختبار قواعد الأعمال دون تشغيل خادم، ولا يمكنك تبديل البنية التحتية دون إعادة كتابة كل شيء. ويحل التقسيم إلى ثلاث طبقات كلتا المشكلتين. تُصرِّح المسارات (Routes) بعناوين URL وتفوّض العمل. وتترجم المتحكمات (Controllers) بروتوكول HTTP إلى استدعاءات أعمال: قراءة الطلب، واختيار رمز الحالة، وتسلسل الاستجابة. وتحمل الخدمات (Services) قواعد الأعمال ولا تعرف شيئًا على الإطلاق عن Express. وقاعدة التبعية صارمة: كل طبقة تعرف فقط الطبقة التي تحتها، وليس العكس أبدًا.

JAVASCRIPT
import { Router } from 'express';

export function createUserRouter(controller) {
  const router = Router();
  router.get('/', controller.list);
  router.post('/', controller.create);
  return router;
}
المسار يُصرِّح، والمتحكم ينفّذ: لا منطق هنا.

إن المتحكم مصنع (factory) يتلقّى الخدمة كمُعطى. وهو لا يحمل أيّ قاعدة أعمال: فمهمته تقتصر على استخراج البيانات من req، واستدعاء الخدمة، وتحويل النتيجة إلى استجابة HTTP. ويُرسَل أيّ خطأ إلى next لتعالجه البرمجية الوسيطة (middleware) المركزية للأخطاء - وليس أبدًا res.status(500) مبعثرة عبر المتحكمات.

JAVASCRIPT
export function createUserController(service) {
  return {
    async list(req, res, next) {
      try {
        const users = await service.listUsers();
        res.json(users);
      } catch (err) {
        next(err);
      }
    },
    async create(req, res, next) {
      try {
        const user = await service.createUser(req.body);
        res.status(201).json(user);
      } catch (err) {
        next(err);
      }
    },
  };
}
ترجمة HTTP خالصة: رموز الحالة، والتسلسل، والتفويض.

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

JAVASCRIPT
export function createUserService(repository) {
  return {
    listUsers() {
      return repository.findAll();
    },
    createUser(input) {
      const found = repository.findByEmail(input.email);
      if (found) {
        const err = new Error('Email already used');
        err.status = 409;
        throw err;
      }
      return repository.insert(input);
    },
  };
}
منطق الأعمال يعيش هنا، دون req أو res أو SQL.

يبقى جذر التركيب (composition root): المكان الوحيد الذي تتجمّع فيه المصانع معًا. تتلقّى createApp تبعيات البنية التحتية (هنا قاعدة البيانات، التي سنتناولها في الدرس التالي) وتُرجِع تطبيق Express كاملًا لا ينصت بعد. ويبدو فصل الإنشاء عن الإنصات أمرًا تافهًا؛ لكنه بالضبط ما سيتيح للاختبارات تركيب واجهة برمجة التطبيقات (API) على منفذ عابر بينما يبقى خادم الإنتاج في ثلاثة أسطر: إنشاء قاعدة البيانات، وإنشاء التطبيق، والإنصات.

JAVASCRIPT
import express from 'express';
import { createUserRepository } from './db/users.js';
import { createUserService } from './services/users.js';
import {
  createUserController,
} from './controllers/users.js';
import { createUserRouter } from './routes/users.js';

export function createApp({ db }) {
  const app = express();
  app.use(express.json());
  const repository = createUserRepository(db);
  const service = createUserService(repository);
  const controller = createUserController(service);
  app.use('/api/users', createUserRouter(controller));
  return app;
}
app.js: الجزء الوحيد من التطبيق الذي يعرف الجميع.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. في هذا التقسيم الطبقي، أين ينبغي أن توجد القاعدة "لا يمكن استخدام بريد إلكتروني إلا مرة واحدة"؟
    • في المسار، أقرب ما يكون إلى عنوان URL
    • في المتحكم، الذي يمكنه الوصول إلى req.body
    • في الخدمة، التي تحمل قواعد الأعمال
    • في برمجية وسيطة (middleware) شاملة في Express
  2. ما الفائدة الرئيسية من تمرير التبعيات كمُعطيات للمصنع بدلًا من استيرادها مباشرة في كل وحدة (module)؟
    • المصانع تُسرِّع تحميل الوحدات
    • يمكنك تبديل تبعية (قاعدة بيانات في الذاكرة، أو مستودع وهمي) دون المساس بالوحدة التي تستخدمها
    • يتطلب Express هذا الأسلوب لبرمجياته الوسيطة
    • يتجنّب كتابة ملفات منفصلة
  3. لماذا لا تبدأ createApp الإنصات على الشبكة بنفسها؟
    • لأن Express يمنع استدعاء listen داخل دالة
    • لكي تتمكن الاختبارات من إنشاء التطبيق دون فتح منفذ ثابت، مع إبقاء نقطة دخول الخادم بسيطة
    • لأن listen غير متزامنة وستُعطِّل المصنع
    • لتقليل استخدام الذاكرة عند بدء التشغيل