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

كنت أظن أن عملية إعادة الهيكلة (Refactoring) للـ Backend ستكون في الغالب مجرد إعادة كتابة للكود السيء ليصبح كوداً "نظيفاً".
كنت مخطئاً.
الجزء الأصعب كان التأكد من ألا يلاحظ أحد أن عملية إعادة الهيكلة تحدث من الأساس.
عملت مؤخراً على إعادة هيكلة حقيقية لخوادم (Express Backend) لنظام تسجيل الحضور بالـ QR في إحدى الجامعات. النظام يضم حالياً 2000 مستخدم نشط، بمتوسط 300 عملية تسجيل حضور يومياً، ولا يزال يمر بمراحل الاختبار (Testing) في كليات مختلفة.
حتى في هذا النطاق، كانت المخاطرة عالية: إذا قام الـ API بإرجاع البيانات بشكل غير متوقع، أو تعطل أثناء المحاضرة، فهذا يعني أن سجلات حضور الطلاب ستتأثر مباشرة.
الفوضى التي ورثتها
عندما بدأت في استكشاف الكود المصدري، وجدت هيكلية نمت بشكل عفوي بمرور الوقت. كان الكود يعاني من ديون تقنية (Technical Debt) واضحة:
- ملفات Routes ضخمة: وجدنا ملفات تتجاوز 3000 سطر برمجي.
- تكرار التحقق من الهوية: كان كود الـ JWT Middleware منسوخاً يدوياً داخل عدة ملفات بشكل متكرر.
- حساء قواعد البيانات: كان المشروع يستخدم
pg(للاستعلامات المباشرة) وknex(كـ Query Builder) وPrisma(كـ ORM)، جميعها مدمجة في نفس المشروع! - تسريب منطق الأعمال: كان الـ Business Logic مكتوباً مباشرة داخل دوال الـ Express Routes بدلاً من عزله.
لكن كان هناك خيار معماري واحد لفت انتباهي بشدة.
مشكلة الـ Prisma Connection Pool
أثناء مراجعة ملفات الـ Routes، لاحظت هذا النمط يتكرر في كل مكان:
// داخل ملف studentRoutes.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient(); // ❌ يتم إنشاء نسخة جديدة في كل ملف
router.post('/record', async (req, res) => {
// ...
});
// داخل ملف attendanceRoutes.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient(); // ❌ Pool اتصالات مستقل تماماًكل ملف Route كان يقوم بإنشاء new PrismaClient() خاص به. هذا يعني أن التطبيق كان يفتح مجموعات اتصالات (Connection Pools) مستقلة ومتعددة بقاعدة البيانات عن غير قصد. أثناء عمليات تسجيل الحضور المتزامنة في بداية المحاضرة، يؤدي هذا النهج إلى إهدار الاتصالات ويزيد من خطر الوصول للحد الأقصى لقاعدة البيانات بشكل مبكر.
كان الحل مباشراً: استخراج نسخة واحدة من Prisma (نمط الـ Singleton) وإعادة استخدامها في جميع أنحاء التطبيق.
// prisma/client.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
module.exports = prisma; // ✅ Pool واحد عامهذا التغيير البرمجي البسيط جعل استهلاك اتصالات قاعدة البيانات أكثر قابلية للتوقع، وقلل من خطر الوصول للحد الأقصى للاتصالات في أوقات الذروة.
نصيحة عملية: لا تقم أبداً بإنشاء نسخة جديدة من الـ ORM الخاص بك داخل ملفات الـ Routes أو الخدمات المنفصلة. استخدم دائماً Singleton للحفاظ على Connection Pool واحد للتطبيق بالكامل في بيئة Node.js.
"الكود النظيف" هو الجزء الأسهل
إدراك آخر وصلت إليه خلال عملية الـ Refactor: تطبيق هيكلية نظيفة (Clean Architecture) هو في الواقع الجزء السهل. التحدي الحقيقي هو القيام بذلك مع:
- الحفاظ على التوافقية (Backward Compatibility): الواجهة الأمامية تتوقع مفاتيح JSON معينة تماماً.
- عدم كسر تطبيق الموبايل: لا يمكنك إجبار مستخدمي الموبايل على تحديث التطبيق فورياً.
- الحفاظ على شكل الرد (Response Behavior): حتى لو تغير شكل قاعدة البيانات، يجب أن تظل مخرجات الـ API مطابقة للنسخة القديمة.
- إضافة الاختبارات للعمليات الهشة: محاولة كتابة اختبارات لأكواد لم تُصمم أصلاً لتكون قابلة للاختبار.
- تصميم مسارات التراجع (Rollback Paths): وجود خطة واضحة للتراجع إذا تسبب التحديث في أخطاء غير متوقعة.
إعادة الهيكلة حسب "مستوى المخاطرة"
لإدارة ذلك، بدأت في تصنيف الميزات بناءً على "مستوى مخاطر الـ Refactor".
على سبيل المثال، تعاملت مع عمليات الحضور والمحاضرات (Attendance & Session flows) كمناطق عالية المخاطر، وتجنبت تعديلها قبل إضافة اختبارات تكامل (Integration Tests) أولاً. لأن هذه العمليات تؤثر على سجلات الطلاب الحقيقية، أي خطأ هناك لا يترجم إلى مجرد رسالة خطأ في الـ Logs، بل يترجم إلى بيانات أكاديمية غير صحيحة.
أحد أهم عمليات النقل التي عملت عليها كانت استبدال عمليات الـ Transactions المكتوبة بـ SQL اليدوي.
الانتقال إلى Prisma Transactions
كان لدينا كود SQL قديم يبدو هكذا:
// Legacy Knex/PG mixed transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// عمليات إدخال وتحديث معقدة
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}قمت بتحويل هذه العمليات إلى prisma.$transaction. الهدف لم يكن مجرد استخدام أداة حديثة، بل ضمان حماية الأنواع (Type Safety) والتراجعات التلقائية (Automatic Rollbacks)، مع التأكد من أن سلوك الـ API ظل متوافقاً مع التطبيقات التي تعتمد عليه (Compatible with existing clients).
// New Prisma Transaction
const result = await prisma.$transaction(async (tx) => {
const session = await tx.session.update({ /* ... */ });
const attendance = await tx.attendance.create({ /* ... */ });
return attendance;
});الخلاصة
كلما تعمقت أكثر في هذا المشروع، كلما تغيرت نظرتي كلياً.
هندسة الـ Backend لم تعد تبدو وكأنها مجرد "بناء APIs"...
...بل بدأت تبدو وكأنها: إدارة مخاطر تحت قيود بيئة العمل الفعلية.
Need help with your backend?
I build and optimize full-stack apps with Node.js, React, and PostgreSQL. If you have a slow API or need a robust architecture, let's talk.