دليل استخدام Node.js وExpress لبناء تطبيقات ويب سريعة وقابلة للتوسع

يُعدّ Node.js بيئة تشغيل تعتمد على محرك V8 الخاص بمتصفح Chrome، وتتيح تنفيذ JavaScript خارج المتصفح. وبفضل نموذج الإدخال والإخراج غير المتزامن وغير الحاجب، يمكن لـ Node.js التعامل مع عدد كبير من الاتصالات المتزامنة بكفاءة، خصوصاً في تطبيقات الويب التي تعتمد على واجهات API، والاتصالات الفورية، وخدمات الشبكات.
أما Express فهو إطار عمل خفيف يُبنى فوق Node.js، ويوفر أدوات واضحة لإدارة المسارات، والطلبات، والاستجابات، والبرمجيات الوسيطة، ومعالجة الأخطاء. وعند استخدامهما وفق بنية سليمة، يمكن بناء تطبيقات سريعة، قابلة للصيانة، وقابلة للتوسع الأفقي والرأسي.
لماذا Node.js وExpress؟
نموذج التنفيذ غير الحاجب
في الخوادم التقليدية، قد يؤدي تنفيذ عملية بطيئة، مثل قراءة ملف أو انتظار قاعدة بيانات، إلى حجز الخيط التنفيذي حتى انتهاء العملية. أما Node.js فيعتمد على حلقة أحداث Event Loop، فتُرسل العمليات البطيئة إلى النظام أو إلى مجموعة خيوط داخلية، بينما يواصل الخادم استقبال طلبات أخرى.
يوضح المثال التالي عملية غير متزامنة:
const fs = require('node:fs');
console.log('بدء القراءة');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
console.log('يستمر التنفيذ');
ستظهر عبارة يستمر التنفيذ قبل محتوى الملف، لأن Node.js لا ينتظر اكتمال القراءة قبل متابعة تنفيذ التعليمات التالية.
مناسب لتطبيقات الشبكات
يتميز Node.js في التطبيقات التي تتضمن:
- واجهات REST وGraphQL.
- تطبيقات الدردشة والاتصالات الفورية.
- خدمات البث ومعالجة الأحداث.
- بوابات API.
- تطبيقات تعتمد على عدد كبير من الطلبات القصيرة.
- الخدمات المصغرة Microservices.
لكن ذلك لا يعني أنه الخيار الأفضل لكل الحالات. فالعمليات الحسابية الثقيلة التي تستهلك المعالج، مثل ضغط الفيديو أو معالجة الصور الكبيرة، قد تعطل حلقة الأحداث ما لم تُنقل إلى عمليات منفصلة أو خدمات متخصصة.
فوائد Express
يوفر Express طبقة منظمة لبناء خادم HTTP، ومن أهم ميزاته:
- تعريف المسارات بسهولة.
- دعم البرمجيات الوسيطة Middleware.
- فصل منطق التوجيه عن منطق الأعمال.
- التعامل مع الرؤوس والاستجابات وحالات HTTP.
- سهولة دمج قواعد البيانات وأنظمة المصادقة.
- مجتمع واسع وحزم متوافقة مع Node.js.
تجهيز بيئة التطوير
تثبيت Node.js
يفضل استخدام إصدار LTS، لأنه أكثر استقراراً وملاءمة للبيئات الإنتاجية. بعد التثبيت، تحقق من الإصدارات:
node --version
npm --version
يُنصح باستخدام مدير إصدارات مثل nvm لتبديل إصدارات Node.js بين المشاريع. مثال:
nvm install --lts
nvm use --lts
إنشاء المشروع
أنشئ مجلداً جديداً ثم هيّئ مشروع npm:
mkdir scalable-api
cd scalable-api
npm init -y
ثبّت Express:
npm install express
ولبيئة التطوير، يمكن تثبيت أداة لإعادة تشغيل الخادم تلقائياً:
npm install --save-dev nodemon
أضف الأوامر التالية إلى ملف package.json:
{
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
}
}
بناء أول خادم باستخدام Express
أنشئ مجلد src وملفاً باسم server.js:
mkdir src
touch src/server.js
ثم أضف الكود التالي:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.get('/', (req, res) => {
res.json({
message: 'الخادم يعمل بنجاح'
});
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
شغّل الخادم:
npm run dev
يمكن اختبار المسار عبر:
curl http://localhost:3000
تستقبل الدالة express() تطبيق Express، بينما يحدد app.get() مساراً من نوع GET. وتسمح express.json() بتحليل أجسام الطلبات التي تحتوي على JSON.
فهم البرمجيات الوسيطة Middleware
البرمجية الوسيطة دالة تُنفَّذ بين وصول الطلب وإرسال الاستجابة. ويمكنها:
- قراءة الطلب أو تعديله.
- التحقق من المصادقة.
- تسجيل الطلبات.
- التحقق من المدخلات.
- إيقاف الطلب وإرسال استجابة.
- تمرير التنفيذ إلى الوسيطة التالية باستخدام
next().
البنية الأساسية:
function exampleMiddleware(req, res, next) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
app.use(exampleMiddleware);
إذا لم تستدعِ الدالة next() ولم ترسل استجابة، فسيظل الطلب معلقاً.
برمجية وسيطة لتسجيل الطلبات
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`
});
});
next();
};
app.use(requestLogger);
في المشاريع الإنتاجية، يمكن استخدام مكتبات مثل pino-http أو morgan، مع تجنب تسجيل بيانات حساسة مثل كلمات المرور ورموز المصادقة.
ترتيب البرمجيات الوسيطة
ترتيب التسجيل مهم. فهذه البرمجية:
app.use(express.json());
app.use('/api/users', userRoutes);
تعني أن تحليل JSON يتم قبل وصول الطلب إلى مسارات المستخدمين. أما إذا سُجّل المسار قبل express.json() فقد لا تتوفر البيانات في req.body عند الحاجة إليها.
تنظيم بنية المشروع
يؤدي وضع جميع المسارات والمنطق في ملف واحد إلى صعوبة الصيانة. يمكن اعتماد بنية عملية مثل:
project/
├── src/
│ ├── app.js
│ ├── server.js
│ ├── config/
│ │ └── env.js
│ ├── routes/
│ │ └── user.routes.js
│ ├── controllers/
│ │ └── user.controller.js
│ ├── services/
│ │ └── user.service.js
│ ├── repositories/
│ │ └── user.repository.js
│ ├── middlewares/
│ │ ├── error.middleware.js
│ │ └── auth.middleware.js
│ └── validators/
│ └── user.validator.js
├── tests/
├── .env
├── .env.example
└── package.json
الفصل بين app.js وserver.js
يحتوي app.js على إعداد التطبيق والمسارات، بينما يتولى server.js تشغيل الخادم. هذا الفصل يسهل اختبار التطبيق دون فتح منفذ شبكي.
ملف app.js:
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok'
});
});
module.exports = app;
ملف server.js:
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
بناء المسارات بطريقة قابلة للتوسع
أنشئ ملفاً للمسارات:
const express = require('express');
const userController = require('../controllers/user.controller');
const router = express.Router();
router.get('/', userController.listUsers);
router.get('/:id', userController.getUser);
router.post('/', userController.createUser);
module.exports = router;
ثم اربطه في app.js:
const userRoutes = require('./routes/user.routes');
app.use('/api/v1/users', userRoutes);
أصبح لدينا الآن:
GET /api/v1/users- `GET /api (Incomplete: max_output_tokens)
