هيكِل واجهة برمجة التطبيقات (API) في طبقات ذات مسؤولية واحدة، واربطها معًا بحقن خفيف لجعلها قابلة للاختبار.
افتح هذا الدرس في Kodokonإن الملف الواحد الذي يخلط بين التوجيه ومنطق الأعمال والوصول إلى البيانات يصبح مكلفًا دائمًا: فلا يمكنك اختبار قواعد الأعمال دون تشغيل خادم، ولا يمكنك تبديل البنية التحتية دون إعادة كتابة كل شيء. ويحل التقسيم إلى ثلاث طبقات كلتا المشكلتين. تُصرِّح المسارات (Routes) بعناوين URL وتفوّض العمل. وتترجم المتحكمات (Controllers) بروتوكول HTTP إلى استدعاءات أعمال: قراءة الطلب، واختيار رمز الحالة، وتسلسل الاستجابة. وتحمل الخدمات (Services) قواعد الأعمال ولا تعرف شيئًا على الإطلاق عن Express. وقاعدة التبعية صارمة: كل طبقة تعرف فقط الطبقة التي تحتها، وليس العكس أبدًا.
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) مبعثرة عبر المتحكمات.
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);
}
},
};
}تركّز الخدمة القرارات: تفرّد البريد الإلكتروني، وقواعد الإنشاء. وهي تُطلق أخطاء الأعمال - مُثراة على الأكثر بحقل status - بينما لا تعرف شيئًا عن Express. وهي أيضًا تتلقّى تبعيتها، وهي مستودع البيانات، كمُعطى. وهذا هو الحقن الخفيف: دوال مصنع بسيطة تُستدعى مرة واحدة عند بدء التشغيل تحل محل حاويات الحقن الثقيلة، وتجعل كل طبقة قابلة للتبديل في الاختبارات.
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);
},
};
}يبقى جذر التركيب (composition root): المكان الوحيد الذي تتجمّع فيه المصانع معًا. تتلقّى createApp تبعيات البنية التحتية (هنا قاعدة البيانات، التي سنتناولها في الدرس التالي) وتُرجِع تطبيق Express كاملًا لا ينصت بعد. ويبدو فصل الإنشاء عن الإنصات أمرًا تافهًا؛ لكنه بالضبط ما سيتيح للاختبارات تركيب واجهة برمجة التطبيقات (API) على منفذ عابر بينما يبقى خادم الإنتاج في ثلاثة أسطر: إنشاء قاعدة البيانات، وإنشاء التطبيق، والإنصات.
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;
}createApp الإنصات على الشبكة بنفسها؟