توثيق Express.js

دليل شامل لتعلم واستخدام إطار عمل Express.js

مقدمة

Express.js هو إطار عمل ويب مرن ومينيمالي لـ Node.js يوفر مجموعة قوية من الميزات لتطوير تطبيقات الويب وواجهات برمجة التطبيقات (APIs). تم تصميمه ليكون بسيطًا وسهل الاستخدام، مما يجعله اختيارًا شائعًا للمطورين الذين يريدون بناء تطبيقات ويب سريعة وقابلة للتطوير.

لماذا Express.js؟

  • بسيط وسريع: تصميم مينيمالي يجعله خفيف الوزن وسريع الأداء.
  • مرونة: لا يفرض بنية معينة على تطبيقك، مما يتيح لك الحرية في الهيكلة.
  • منظومة وسائط (Middleware): نظام وسائط قوي لمعالجة الطلبات.
  • التوجيه: آلية توجيه بسيطة وفعالة للتعامل مع مختلف الطلبات HTTP.
  • محركات القوالب: دعم للعديد من محركات القوالب الشائعة.
  • بيئة Node.js: الاستفادة من جميع مزايا النظام البيئي لـ Node.js.

ملاحظة

Express هو جزء من منظومة MEAN/MERN stack (MongoDB, Express, Angular/React, Node.js) المستخدمة على نطاق واسع لتطوير تطبيقات الويب الكاملة.

التثبيت والإعداد

المتطلبات الأساسية

قبل البدء في استخدام Express.js، تحتاج إلى تثبيت:

  • Node.js (النسخة 12.x أو أحدث)
  • npm (عادة ما يأتي مع تثبيت Node.js)

إنشاء مشروع جديد

لإنشاء مشروع Express جديد، اتبع الخطوات التالية:

# إنشاء مجلد جديد للمشروع
mkdir my-express-app
cd my-express-app

# تهيئة مشروع npm
npm init -y

# تثبيت Express
npm install express

إنشاء تطبيق أساسي

بعد تثبيت Express، يمكنك إنشاء تطبيق بسيط. قم بإنشاء ملف app.js في مجلد المشروع:

const express = require('express');
const app = express();
const port = 3000;

// طريق الصفحة الرئيسية
app.get('/', (req, res) => {
  res.send('مرحباً بالعالم من Express!');
});

// بدء الخادم
app.listen(port, () => {
  console.log(`التطبيق يعمل على المنفذ ${port}`);
});

تشغيل التطبيق

لتشغيل التطبيق، قم بتنفيذ الأمر التالي:

node app.js

بعد تنفيذ الأمر، يمكنك فتح المتصفح وزيارة http://localhost:3000 لرؤية رسالة "مرحباً بالعالم من Express!".

استخدام nodemon للتطوير

يمكنك استخدام nodemon لإعادة تشغيل التطبيق تلقائيًا عند تغيير الكود:

# تثبيت nodemon عالمياً
npm install -g nodemon

# تشغيل التطبيق باستخدام nodemon
nodemon app.js

نصيحة

يمكنك أيضاً تثبيت nodemon كتبعية تطوير وإضافة سكريبت في ملف package.json:

"scripts": {
  "start": "node app.js",
  "dev": "nodemon app.js"
}
                            

ثم يمكنك تشغيل التطبيق في وضع التطوير باستخدام npm run dev.

أساسيات Express

تطبيق Express

تطبيق Express هو كائن من الدالة express(). يتم استخدامه لإعداد الخادم وتكوين الطرق ومعالجات الوسائط والإعدادات الأخرى.

const express = require('express');
const app = express();

طرق HTTP

يدعم Express جميع طرق HTTP الشائعة مثل: GET، POST، PUT، DELETE، إلخ.

// طلب GET
app.get('/users', (req, res) => {
  res.send('قائمة المستخدمين');
});

// طلب POST
app.post('/users', (req, res) => {
  res.send('تم إنشاء مستخدم جديد');
});

// طلب PUT
app.put('/users/:id', (req, res) => {
  res.send(`تم تحديث المستخدم ${req.params.id}`);
});

// طلب DELETE
app.delete('/users/:id', (req, res) => {
  res.send(`تم حذف المستخدم ${req.params.id}`);
});

كائنات الطلب والاستجابة

في Express، يتم تمرير كائني request و response لمعالجات الطلبات.

كائن الطلب (Request)

يحتوي على معلومات عن الطلب HTTP، مثل:

  • req.params - المعلمات المستخرجة من URL
  • req.query - معلمات الاستعلام من URL
  • req.body - بيانات الجسم من الطلب
  • req.headers - رؤوس HTTP
  • req.cookies - ملفات تعريف الارتباط

كائن الاستجابة (Response)

يستخدم لإرسال الاستجابة HTTP إلى العميل، ويوفر طرقًا مثل:

  • res.send() - إرسال استجابة
  • res.json() - إرسال استجابة JSON
  • res.render() - تقديم قالب
  • res.redirect() - إعادة توجيه الطلب
  • res.status() - تعيين رمز حالة HTTP
// استخدام معلمات URL ومعلمات الاستعلام
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  const sortBy = req.query.sortBy || 'name';
  
  res.json({
    userId: userId,
    sortBy: sortBy,
    message: `عرض المستخدم ${userId} مرتبًا حسب ${sortBy}`
  });
});

// إرسال استجابات مختلفة
app.get('/examples/text', (req, res) => {
  res.send('استجابة نصية بسيطة');
});

app.get('/examples/json', (req, res) => {
  res.json({ message: 'استجابة JSON' });
});

app.get('/examples/status', (req, res) => {
  res.status(201).send('تم إنشاء المورد');
});

app.get('/examples/redirect', (req, res) => {
  res.redirect('/examples/json');
});

التوجيه (Routing)

التوجيه في Express يشير إلى كيفية استجابة تطبيقك للطلب من العميل على مسار (URL) وطريقة HTTP معينة.

التوجيه الأساسي

التوجيه الأساسي في Express يتم تعريفه باستخدام طرق تطبيق Express التي تتطابق مع طرق HTTP. يتكون كل طريق من مسار URL ودالة معالج واحدة أو أكثر.

// تعريف طريق بسيط
app.get('/hello', (req, res) => {
  res.send('مرحباً بالعالم');
});

// طريق بمعلمات URL
app.get('/users/:userId', (req, res) => {
  res.send(`معلومات المستخدم: ${req.params.userId}`);
});

معلمات URL

تستخدم معلمات URL لالتقاط القيم المحددة في عنوان URL. يمكن الوصول إليها عبر req.params.

// معلمة URL إلزامية
app.get('/products/:category/:id', (req, res) => {
  const { category, id } = req.params;
  res.send(`المنتج: ${id} في التصنيف: ${category}`);
});

// معلمة URL اختيارية
app.get('/products/:category?', (req, res) => {
  const category = req.params.category || 'جميع المنتجات';
  res.send(`عرض ${category}`);
});

استخدام التعبيرات النمطية (Regular Expressions)

يمكنك استخدام التعبيرات النمطية في المسارات للتحكم بشكل أدق في المطابقة:

// التحقق من أن المعلمة عبارة عن ملف PDF
app.get(/.*\.pdf$/, (req, res) => {
  res.send('محتوى ملف PDF');
});

معلمات الاستعلام

معلمات الاستعلام (Query Parameters) تأتي في نهاية URL بعد علامة الاستفهام (؟) ويمكن الوصول إليها عبر req.query.

// استخدام معلمات الاستعلام للبحث
app.get('/search', (req, res) => {
  const query = req.query.q;
  const limit = req.query.limit || 10;
  
  res.send(`نتائج البحث عن: ${query} (عرض ${limit} نتيجة)`);
});

استخدام Router

يمكنك تنظيم المسارات في ملفات منفصلة باستخدام express.Router، مما يساعد في الحفاظ على كود أكثر تنظيماً:

// في ملف routes/users.js
const express = require('express');
const router = express.Router();

// مسارات المستخدمين
router.get('/', (req, res) => {
  res.send('قائمة المستخدمين');
});

router.get('/:id', (req, res) => {
  res.send(`عرض المستخدم: ${req.params.id}`);
});

router.post('/', (req, res) => {
  res.send('إنشاء مستخدم جديد');
});

// تصدير الموجه
module.exports = router;

// في ملف app.js
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);

الوسائط (Middleware)

الوسائط (Middleware) هي دوال تحصل على الوصول إلى كائنات الطلب (req) والاستجابة (res) ودالة next في دورة طلب-استجابة التطبيق. يمكن للوسائط:

  • تنفيذ أي كود
  • إجراء تغييرات على كائنات الطلب والاستجابة
  • إنهاء دورة الطلب-الاستجابة
  • استدعاء الوسيط التالي في السلسلة

وسيط بسيط

مثال على وسيط بسيط لتسجيل معلومات الطلب:

const loggerMiddleware = (req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next(); // استدعاء الوسيط التالي
};

// استخدام الوسيط على مستوى التطبيق
app.use(loggerMiddleware);

أنواع الوسائط

يمكن تطبيق الوسائط على مستويات مختلفة:

1. وسيط على مستوى التطبيق

// يتم تطبيقه على جميع الطلبات
app.use(middlewareFunction);

2. وسيط خاص بمسار معين

// يتم تطبيقه فقط على الطلبات التي تستهدف مسار محدد
app.use('/api', middlewareFunction);

3. وسيط خاص بمعالج طريق معين

// يتم تطبيقه فقط على معالج محدد
app.get('/products', middlewareFunction, (req, res) => {
  res.send('قائمة المنتجات');
});

وسائط Express المدمجة الشائعة

1. معالجة بيانات JSON

لتحليل طلبات JSON القادمة وتوفيرها في req.body:

app.use(express.json());

2. معالجة بيانات النموذج

لتحليل بيانات النموذج (form data) وتوفيرها في req.body:

app.use(express.urlencoded({ extended: true }));

3. خدمة الملفات الثابتة

لخدمة الملفات الثابتة مثل الصور وملفات CSS و JavaScript:

app.use(express.static('public'));

ملاحظة

ترتيب تعريف الوسائط مهم! يتم استدعاء الوسائط بالترتيب الذي تم تعريفها به. لذلك، عادة ما يتم تعريف الوسائط العامة (مثل تسجيل الطلبات أو تحليل الجسم) قبل معالجات المسارات الخاصة.

محركات القوالب

تسمح محركات القوالب بإنشاء صفحات HTML ديناميكية عن طريق إدخال بيانات في قوالب. يدعم Express العديد من محركات القوالب مثل EJS و Pug و Handlebars والمزيد.

إعداد محرك القوالب

لاستخدام محرك قوالب، تحتاج إلى تثبيته وتكوينه في تطبيقك:

# تثبيت محرك قوالب EJS
npm install ejs
// تكوين Express لاستخدام EJS
app.set('view engine', 'ejs');
app.set('views', './views'); // مجلد لتخزين القوالب

إنشاء قالب

مثال على قالب EJS بسيط (views/index.ejs):




  <%= title %>


  

<%= heading %>

    <% for(let i=0; i < items.length; i++) { %>
  • <%= items[i] %>
  • <% } %>

تقديم القالب

استخدام الدالة res.render() لتقديم القالب مع البيانات:

app.get('/', (req, res) => {
  res.render('index', {
    title: 'صفحة Express الرئيسية',
    heading: 'مرحباً بالعالم!',
    items: ['عنصر 1', 'عنصر 2', 'عنصر 3']
  });
});

نصيحة

قم باستكشاف محركات قوالب مختلفة لاختيار الأنسب لمشروعك. تتميز Pug بصيغة مختصرة، بينما توفر EJS صيغة قريبة من HTML، وتستخدم Handlebars نهجًا حديثًا بالقوالب المستقلة عن المنطق.

الملفات الثابتة

يوفر Express آلية لخدمة الملفات الثابتة مثل الصور وملفات CSS و JavaScript عبر الوسيط express.static.

خدمة الملفات من مجلد

لخدمة ملفات من مجلد public:

app.use(express.static('public'));

بهذه الطريقة، يمكن الوصول إلى الملفات الموجودة في المجلد public مباشرة من الجذر:

  • http://localhost:3000/images/logo.png يخدم الملف public/images/logo.png
  • http://localhost:3000/css/style.css يخدم الملف public/css/style.css
  • http://localhost:3000/js/app.js يخدم الملف public/js/app.js

استخدام بادئة للمسار

يمكنك أيضًا إضافة بادئة للمسار الافتراضي:

app.use('/static', express.static('public'));

الآن، يمكن الوصول إلى الملفات باستخدام البادئة:

  • http://localhost:3000/static/images/logo.png
  • http://localhost:3000/static/css/style.css

ملاحظة أمنية

من المهم عدم وضع ملفات حساسة في المجلدات المخدمة بشكل ثابت، حيث يمكن للمستخدمين الوصول إلى جميع الملفات الموجودة في هذه المجلدات. حدد بعناية الملفات التي تريد مشاركتها عبر هذه الآلية.

معالجة الأخطاء

تعتبر معالجة الأخطاء مهمة في تطبيقات Express لضمان تجربة مستخدم جيدة وتوفير سلوك متوقع في حالات الفشل.

معالجة أساسية للأخطاء

في Express، يتم تعريف وسائط معالجة الأخطاء بأربعة معلمات (بدلاً من ثلاثة كالوسائط العادية):

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('حدث خطأ ما!');
});

معالجة 404 (غير موجود)

لمعالجة المسارات غير الموجودة، أضف وسيطًا في نهاية سلسلة الوسائط:

// يجب أن يكون هذا الوسيط بعد جميع المسارات الأخرى
app.use((req, res, next) => {
  res.status(404).send("عذراً، لم نتمكن من العثور على ما تبحث عنه!");
});

معالجة أخطاء غير متزامنة

للتعامل مع الأخطاء في دوال غير متزامنة، يمكنك:

// استخدام try/catch مع async/await
app.get('/async-data', async (req, res, next) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (error) {
    next(error); // تمرير الخطأ إلى معالج الأخطاء
  }
});

// أو استخدام وعود
app.get('/promise-data', (req, res, next) => {
  fetchData()
    .then(data => res.json(data))
    .catch(next); // تمرير الخطأ مباشرة إلى next
});

معالجة أخطاء متقدمة

يمكنك إنشاء بنية أكثر تعقيدًا لمعالجة الأخطاء المختلفة:

// تعريف أنواع أخطاء مخصصة
class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NotFoundError';
    this.statusCode = 404;
  }
}

class ValidationError extends Error {
  constructor(message, validationErrors) {
    super(message);
    this.name = 'ValidationError';
    this.statusCode = 400;
    this.validationErrors = validationErrors;
  }
}

// رمي أخطاء في معالجات المسارات
app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  
  if (!user) {
    return next(new NotFoundError('المستخدم غير موجود'));
  }
  
  res.json(user);
});

// معالج أخطاء شامل
app.use((err, req, res, next) => {
  console.error(err);
  
  const statusCode = err.statusCode || 500;
  const errorResponse = {
    error: {
      message: err.message || 'حدث خطأ داخلي في الخادم',
      type: err.name || 'Error'
    }
  };
  
  // إضافة تفاصيل التحقق إذا كانت موجودة
  if (err.validationErrors) {
    errorResponse.error.validationErrors = err.validationErrors;
  }
  
  // في بيئة التطوير فقط، أضف معلومات التتبع
  if (process.env.NODE_ENV === 'development') {
    errorResponse.error.stack = err.stack;
  }
  
  res.status(statusCode).json(errorResponse);
});

قواعد البيانات

يمكن استخدام Express مع مجموعة متنوعة من قواعد البيانات، سواء كانت قواعد بيانات SQL مثل MySQL و PostgreSQL و SQLite، أو قواعد بيانات NoSQL مثل MongoDB.

الاتصال بقواعد البيانات

1. MongoDB مع Mongoose

// تثبيت: npm install mongoose

const mongoose = require('mongoose');

// الاتصال بقاعدة البيانات
mongoose.connect('mongodb://localhost:27017/myapp', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
  .then(() => console.log('تم الاتصال بقاعدة البيانات MongoDB بنجاح'))
  .catch(err => console.error('خطأ في الاتصال بقاعدة البيانات:', err));

// تعريف مخطط البيانات (Schema)
const userSchema = new mongoose.Schema({
  name: String,
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now }
});

// إنشاء نموذج
const User = mongoose.model('User', userSchema);

// استخدام النموذج في المسارات
app.post('/users', async (req, res, next) => {
  try {
    const newUser = new User(req.body);
    const savedUser = await newUser.save();
    res.status(201).json(savedUser);
  } catch (error) {
    next(error);
  }
});

2. MySQL مع mysql2

// تثبيت: npm install mysql2

const mysql = require('mysql2/promise');

// إنشاء تجمع اتصالات
const pool = mysql.createPool({
  host: 'localhost',
  user: 'user',
  password: 'password',
  database: 'myapp',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

// استخدام تجمع الاتصالات في المسارات
app.get('/users', async (req, res, next) => {
  try {
    const [rows] = await pool.query('SELECT * FROM users');
    res.json(rows);
  } catch (error) {
    next(error);
  }
});

3. PostgreSQL مع pg

// تثبيت: npm install pg

const { Pool } = require('pg');

const pool = new Pool({
  user: 'user',
  host: 'localhost',
  database: 'myapp',
  password: 'password',
  port: 5432,
});

app.get('/users/:id', async (req, res, next) => {
  try {
    const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
    
    if (rows.length === 0) {
      return res.status(404).json({ message: 'المستخدم غير موجود' });
    }
    
    res.json(rows[0]);
  } catch (error) {
    next(error);
  }
});

4. SQLite مع better-sqlite3

// تثبيت: npm install better-sqlite3

const Database = require('better-sqlite3');
const db = new Database('myapp.db', { verbose: console.log });

// إنشاء جدول إذا لم يكن موجودًا
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    email TEXT UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )
`);

app.post('/users', (req, res) => {
  try {
    const { name, email } = req.body;
    const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
    const result = stmt.run(name, email);
    
    res.status(201).json({
      id: result.lastInsertRowid,
      name,
      email
    });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

ORMs و Query Builders

لتبسيط التفاعل مع قواعد البيانات، يمكنك استخدام ORM (Object-Relational Mapping) أو أدوات بناء الاستعلامات:

  • Sequelize: ORM لـ MySQL و PostgreSQL و SQLite وغيرها
  • TypeORM: ORM مع دعم TypeScript
  • Prisma: ORM حديث مع واجهة برمجة قوية
  • Knex.js: مُنشئ استعلامات SQL مرن

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

تعد المصادقة والتفويض من المتطلبات الرئيسية للعديد من تطبيقات الويب. هناك عدة طرق لتنفيذ المصادقة في Express، ولكن أحد الخيارات الشائعة هو استخدام Passport.js.

Passport.js

Passport هي مكتبة مصادقة وسيطة لـ Node.js، تعمل بشكل جيد مع Express وتدعم العديد من استراتيجيات المصادقة.

// تثبيت: npm install passport passport-local express-session

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');

// إعداد express-session
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false
}));

// تهيئة Passport
app.use(passport.initialize());
app.use(passport.session());

// إعداد استراتيجية المصادقة المحلية
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  async (email, password, done) => {
    try {
      // البحث عن المستخدم في قاعدة البيانات
      const user = await User.findOne({ email: email });
      
      if (!user) {
        return done(null, false, { message: 'البريد الإلكتروني غير صحيح' });
      }
      
      // التحقق من كلمة المرور (باستخدام دالة مناسبة للتحقق من التشفير)
      const isValid = await validatePassword(password, user.password);
      
      if (!isValid) {
        return done(null, false, { message: 'كلمة المرور غير صحيحة' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// تسلسل/إلغاء تسلسل معلومات المستخدم من/إلى الجلسة
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (error) {
    done(error);
  }
});

// مسارات تسجيل الدخول/الخروج
app.post('/login', passport.authenticate('local', {
  successRedirect: '/dashboard',
  failureRedirect: '/login',
  failureFlash: true // إذا كنت تستخدم connect-flash
}));

app.get('/logout', (req, res) => {
  req.logout(function(err) {
    if (err) { return next(err); }
    res.redirect('/');
  });
});

// وسيط للتحقق من المصادقة
const isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
};

// استخدام وسيط المصادقة لحماية المسارات
app.get('/dashboard', isAuthenticated, (req, res) => {
  res.render('dashboard', { user: req.user });
});

مصادقة JWT (JSON Web Tokens)

يمكن استخدام JWT كبديل لجلسات المستخدم، وهو مفيد بشكل خاص لواجهات برمجة التطبيقات (APIs):

// تثبيت: npm install jsonwebtoken

const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key';

// مسار تسجيل الدخول وإنشاء JWT
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  try {
    // التحقق من المستخدم وكلمة المرور
    const user = await User.findOne({ email });
    
    if (!user || !await validatePassword(password, user.password)) {
      return res.status(401).json({ message: 'بيانات الاعتماد غير صالحة' });
    }
    
    // إنشاء توكن JWT
    const token = jwt.sign(
      { id: user._id, email: user.email },
      secretKey,
      { expiresIn: '1h' }
    );
    
    res.json({ token });
  } catch (error) {
    res.status(500).json({ message: 'خطأ في الخادم' });
  }
});

// وسيط للتحقق من JWT
const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ message: 'الوصول مرفوض. التوكن مطلوب.' });
  }
  
  const token = authHeader.split(' ')[1];
  
  jwt.verify(token, secretKey, (err, user) => {
    if (err) {
      return res.status(403).json({ message: 'الوصول مرفوض. التوكن غير صالح.' });
    }
    
    req.user = user;
    next();
  });
};

// استخدام وسيط JWT لحماية المسارات
app.get('/api/protected', authenticateJWT, (req, res) => {
  res.json({ message: 'هذه بيانات محمية', user: req.user });
});

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

Express مناسب جدًا لبناء واجهات برمجة التطبيقات (APIs). في هذا القسم، سنتناول أفضل الممارسات والتقنيات لتطوير واجهات API RESTful.

تصميم API RESTful

يجب أن تتبع واجهات API مبادئ REST الأساسية:

  • استخدام أسماء الموارد كـ URLs (مثل /users)
  • استخدام طرق HTTP المناسبة (GET للقراءة، POST للإنشاء، PUT/PATCH للتحديث، DELETE للحذف)
  • استخدام أكواد حالة HTTP بشكل مناسب (200 للنجاح، 201 للإنشاء، 400/404 للأخطاء، إلخ)
  • تقديم استجابات متسقة وموثقة
// مثال API RESTful لمورد المستخدمين
const router = express.Router();

// الحصول على قائمة المستخدمين
router.get('/users', async (req, res) => {
  try {
    const users = await User.find({}, '-password'); // استبعاد كلمة المرور
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: 'فشل في استرجاع المستخدمين' });
  }
});

// الحصول على مستخدم واحد بواسطة المعرف
router.get('/users/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id, '-password');
    
    if (!user) {
      return res.status(404).json({ error: 'المستخدم غير موجود' });
    }
    
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'فشل في استرجاع المستخدم' });
  }
});

// إنشاء مستخدم جديد
router.post('/users', async (req, res) => {
  try {
    const { name, email, password } = req.body;
    
    // التحقق من وجود بيانات إلزامية
    if (!name || !email || !password) {
      return res.status(400).json({ error: 'جميع الحقول إلزامية' });
    }
    
    // التحقق من عدم وجود بريد إلكتروني مكرر
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(409).json({ error: 'البريد الإلكتروني موجود بالفعل' });
    }
    
    const newUser = new User({ name, email, password });
    await newUser.save();
    
    // إعادة المستخدم بدون كلمة المرور
    const userResponse = newUser.toObject();
    delete userResponse.password;
    
    res.status(201).json(userResponse);
  } catch (error) {
    res.status(500).json({ error: 'فشل في إنشاء المستخدم' });
  }
});

// تحديث مستخدم
router.put('/users/:id', async (req, res) => {
  try {
    const { name, email } = req.body;
    
    const updatedUser = await User.findByIdAndUpdate(
      req.params.id,
      { name, email },
      { new: true, runValidators: true, select: '-password' }
    );
    
    if (!updatedUser) {
      return res.status(404).json({ error: 'المستخدم غير موجود' });
    }
    
    res.json(updatedUser);
  } catch (error) {
    res.status(500).json({ error: 'فشل في تحديث المستخدم' });
  }
});

// حذف مستخدم
router.delete('/users/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    
    if (!user) {
      return res.status(404).json({ error: 'المستخدم غير موجود' });
    }
    
    res.status(204).send(); // No Content
  } catch (error) {
    res.status(500).json({ error: 'فشل في حذف المستخدم' });
  }
});

التحقق من صحة البيانات

من المهم التحقق من بيانات الإدخال للـ API. يمكنك استخدام مكتبات مثل express-validator لهذا الغرض:

// تثبيت: npm install express-validator

const { body, validationResult } = require('express-validator');

router.post('/users', [
  // قواعد التحقق
  body('name').trim().notEmpty().withMessage('الاسم مطلوب'),
  body('email').isEmail().withMessage('بريد إلكتروني غير صالح')
    .normalizeEmail().custom(async (email) => {
      const user = await User.findOne({ email });
      if (user) {
        throw new Error('البريد الإلكتروني موجود بالفعل');
      }
      return true;
    }),
  body('password').isLength({ min: 6 }).withMessage('كلمة المرور يجب أن تكون 6 أحرف على الأقل')
], async (req, res) => {
  // التحقق من وجود أخطاء
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // متابعة معالجة الطلب
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(500).json({ error: 'فشل في إنشاء المستخدم' });
  }
});

تحسين الأداء والتخزين المؤقت

لتحسين أداء واجهة API، يمكنك استخدام التخزين المؤقت (Caching):

// تثبيت: npm install memory-cache

const cache = require('memory-cache');

// وسيط التخزين المؤقت
const cacheMiddleware = (duration) => {
  return (req, res, next) => {
    const key = '__express__' + req.originalUrl || req.url;
    const cachedBody = cache.get(key);
    
    if (cachedBody) {
      res.send(cachedBody);
      return;
    } else {
      res.sendResponse = res.send;
      res.send = (body) => {
        cache.put(key, body, duration * 1000);
        res.sendResponse(body);
      };
      next();
    }
  };
};

// استخدام التخزين المؤقت لمدة 10 دقائق
app.get('/api/products', cacheMiddleware(600), async (req, res) => {
  const products = await Product.find();
  res.json(products);
});

توثيق API

توثيق واجهة API مهم جدًا. يمكنك استخدام أدوات مثل Swagger/OpenAPI:

// تثبيت: npm install swagger-jsdoc swagger-ui-express

const swaggerJsDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const swaggerOptions = {
  swaggerDefinition: {
    openapi: '3.0.0',
    info: {
      title: 'API الخاصة بتطبيق Express',
      version: '1.0.0',
      description: 'واجهة برمجة التطبيقات RESTful مع Express'
    },
    servers: [
      {
        url: 'http://localhost:3000'
      }
    ]
  },
  apis: ['./routes/*.js'] // ملفات الطرق التي تحتوي على توثيق
};

const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

// مثال على توثيق Swagger في ملف الطرق

/**
 * @swagger
 * /api/users:
 *   get:
 *     summary: استرجاع قائمة المستخدمين
 *     description: استرجاع قائمة بجميع المستخدمين من قاعدة البيانات
 *     responses:
 *       200:
 *         description: قائمة المستخدمين
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 type: object
 *                 properties:
 *                   id:
 *                     type: string
 *                   name:
 *                     type: string
 *                   email:
 *                     type: string
 */

النشر في بيئة الإنتاج

عند تحضير تطبيق Express للإنتاج، هناك عدة اعتبارات مهمة يجب مراعاتها.

ضبط متغيرات البيئة

استخدم متغيرات البيئة لتخزين البيانات الحساسة وإعدادات البيئة المختلفة:

// تثبيت: npm install dotenv

// في ملف app.js
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp';

mongoose.connect(MONGODB_URI)
  .then(() => console.log('تم الاتصال بقاعدة البيانات'))
  .catch(err => console.error('خطأ في الاتصال بقاعدة البيانات:', err));

app.listen(PORT, () => {
  console.log(`الخادم يعمل على المنفذ ${PORT}`);
});

ملف .env:

PORT=3000
MONGODB_URI=mongodb://username:password@host:port/database
JWT_SECRET=your-secret-key
NODE_ENV=production

استخدام مدير العمليات

PM2 هو مدير عمليات إنتاج لتطبيقات Node.js يساعد في الحفاظ على التطبيق قيد التشغيل:

# تثبيت PM2 عالمياً
npm install -g pm2

# بدء تشغيل التطبيق
pm2 start app.js --name "myapp"

# عرض حالة التطبيقات
pm2 status

# إعادة تشغيل التطبيق
pm2 restart myapp

# إيقاف التطبيق
pm2 stop myapp

# تكوين PM2 لإعادة تشغيل التطبيق تلقائياً عند إعادة تشغيل الخادم
pm2 startup
pm2 save

أمان التطبيق

تطبيق ممارسات الأمان الأساسية باستخدام helmet:

// تثبيت: npm install helmet

const helmet = require('helmet');
app.use(helmet()); // يضبط رؤوس HTTP المتعلقة بالأمان

تضغيط الاستجابات

استخدم compression لتقليل حجم الاستجابات:

// تثبيت: npm install compression

const compression = require('compression');
app.use(compression());

تسجيل الأحداث

استخدم مكتبة لتسجيل الأحداث مثل winston لمراقبة التطبيق:

// تثبيت: npm install winston

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// استخدام السجل
logger.info('تم بدء تشغيل الخادم');
logger.error('حدث خطأ', { error: err });

النشر على خدمات الاستضافة

يمكن نشر تطبيقات Express على مختلف منصات الاستضافة:

  • Heroku: سهلة الاستخدام وتدعم Node.js مباشرة
  • AWS: خيارات متعددة مثل EC2 أو Elastic Beanstalk
  • Google Cloud: App Engine أو Compute Engine
  • DigitalOcean: خوادم VPS بأسعار معقولة
  • Microsoft Azure: خدمات App Service لتطبيقات Node.js

أفضل الممارسات

استخدام أفضل الممارسات في Express يمكن أن يساعد في تطوير تطبيقات أكثر استقرارًا وقابلية للصيانة والتوسع.

هيكلة المشروع

تنظيم المشروع بطريقة منطقية تسهل الصيانة:

my-express-app/
├── config/                 # إعدادات التطبيق
│   ├── db.js              # تكوين قاعدة البيانات
│   └── passport.js        # تكوين المصادقة
├── controllers/            # منطق معالجة الطلبات
│   ├── userController.js
│   └── productController.js
├── middlewares/            # وسائط مخصصة
│   ├── auth.js
│   └── errorHandler.js
├── models/                 # نماذج البيانات
│   ├── User.js
│   └── Product.js
├── routes/                 # تعريفات المسارات
│   ├── userRoutes.js
│   └── productRoutes.js
├── utils/                  # وظائف مساعدة
│   ├── logger.js
│   └── validators.js
├── public/                 # ملفات ثابتة
├── views/                  # قوالب العرض
├── app.js                  # نقطة الدخول للتطبيق
├── package.json
└── README.md

التعامل مع الأخطاء

التأكد من معالجة جميع الأخطاء المحتملة بشكل مناسب:

  • استخدام try/catch مع الدوال غير المتزامنة
  • تنفيذ وسائط عامة لمعالجة الأخطاء
  • التعامل مع الحالات الشاذة مثل القيم الفارغة أو غير المتوقعة
  • تسجيل الأخطاء للمراجعة والتصحيح

الأمان

تطبيق إجراءات الأمان الأساسية:

  • استخدام HTTPS في الإنتاج
  • تخزين كلمات المرور بشكل آمن (باستخدام bcrypt)
  • تنفيذ حماية CSRF لتطبيقات الويب التقليدية
  • استخدام لائحة مصادر مشتركة عبر المواقع (CORS) بشكل صحيح
  • تقييد معدل الطلبات لمنع هجمات حجب الخدمة
  • التحقق من دخول المستخدم للبيانات لمنع هجمات حقن SQL وهجمات البرمجة النصية بين المواقع (XSS)
// تثبيت: npm install bcrypt cors express-rate-limit

const bcrypt = require('bcrypt');
const cors = require('cors');
const rateLimit = require('express-rate-limit');

// تخزين كلمات المرور بشكل آمن
const hashPassword = async (password) => {
  const salt = await bcrypt.genSalt(10);
  return await bcrypt.hash(password, salt);
};

// تكوين CORS
app.use(cors({
  origin: 'https://yourdomain.com', // السماح فقط بالطلبات من هذا المصدر
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// تقييد معدل الطلبات
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 دقيقة
  max: 100, // الحد الأقصى للطلبات لكل IP
  message: 'تم تجاوز الحد الأقصى للطلبات، يرجى المحاولة لاحقاً'
});

app.use('/api/', apiLimiter);

الأداء

تحسين أداء التطبيق:

  • استخدام التخزين المؤقت للاستجابات المتكررة
  • ضغط البيانات باستخدام compression
  • استخدام Promises أو async/await للتعامل مع العمليات غير المتزامنة
  • تحسين استعلامات قاعدة البيانات وتنفيذ الفهرسة المناسبة
  • تقليل حجم وعدد طلبات HTTP عن طريق توحيد الملفات الثابتة

اختبار التطبيق

تنفيذ اختبارات شاملة للتطبيق:

// تثبيت: npm install jest supertest

// tests/userRoutes.test.js
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');

beforeEach(async () => {
  await User.deleteMany({});
});

describe('مسارات المستخدم', () => {
  it('يجب إنشاء مستخدم جديد', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'مستخدم اختبار',
        email: '[email protected]',
        password: 'password123'
      });
    
    expect(response.statusCode).toBe(201);
    expect(response.body).toHaveProperty('_id');
    expect(response.body.name).toBe('مستخدم اختبار');
  });
  
  it('يجب استرجاع قائمة المستخدمين', async () => {
    // إنشاء مستخدم اختبار
    await User.create({
      name: 'مستخدم اختبار',
      email: '[email protected]',
      password: 'password123'
    });
    
    const response = await request(app).get('/api/users');
    
    expect(response.statusCode).toBe(200);
    expect(response.body.length).toBe(1);
    expect(response.body[0].name).toBe('مستخدم اختبار');
  });
});