الوسيطات ومعالجة الأخطاء
سلسلة الدوال التي يمر بها كل طلب ⏱ نحو 40 دقيقة.
- تشرح سلسلة الوسطاء ودور
next. - تكتب وسيطًا عامًّا ووسيطًا لمسار واحد.
- تستخدم الوسطاء الجاهزة:
jsonوstaticوcorsوmorgan. - تعالج الأخطاء و404 في مكان واحد بـ
JSONموحّد.
١ما الوسيط؟
الطلب في Express لا يذهب مباشرة إلى معالج المسار، بل يمر على سلسلة من الدوال بالترتيب الذي كُتبت به. كل دالة في السلسلة وسيط Middleware يستقبل ثلاثة معاملات: (req, res, next)، ويفعل واحدًا من أمرين:
- يُنهي الطلب بإرسال رد (
res.jsonمثلًا)، فتتوقف السلسلة. - أو يستدعي
next()ليمرر الطلب إلى الدالة التالية.
تشبه نقاط التفتيش في مطار: الأمن، ثم الجوازات، ثم البوابة. أي نقطة يمكنها أن توقفك، أو تسمح لك بالمرور إلى التالية.
const express = require('express');const app = express(); // وسيط يعمل مع كل طلب: يطبع الطريقة والمسارapp.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next(); // مرّر إلى التالي}); app.get('/', (req, res) => res.send('الرئيسية'));app.get('/about', (req, res) => res.send('من نحن')); app.listen(3000);
افتح أي طلب في السجل: الطرفية تُظهر أن الوسيط طبع سطرًا لكل طلب، حتى الطلب الذي انتهى بـ 404.
٢نسيان next
إن لم يرسل الوسيط ردًّا ولم يستدعِ next() يبقى الطلب معلّقًا: المتصفح ينتظر ولا يصله شيء:
const express = require('express');const app = express(); app.use((req, res, next) => { console.log('وصل طلب'); // نسينا next()}); app.get('/', (req, res) => res.send('لن تصل')); app.listen(3000);
المختبر أوقف الانتظار بعد 3 ثوانٍ وأبلغ أن الخادم لم يرد. أضف next(); وجرّب من جديد.
٣الترتيب مهم، والوسيط على مسار واحد
app.use(fn) يطبّق الوسيط على كل الطلبات التي تأتي بعده في الكود. ويمكن أيضًا وضع وسيط على مسار بعينه، بين المسار والمعالج. هنا وسيط يتحقق من مفتاح سري في الترويسة قبل المسارات المحمية:
const express = require('express');const app = express(); function requireKey(req, res, next) { if (req.headers['x-api-key'] !== 'tvtc-2025') { return res.status(401).json({ error: 'مفتاح غير صالح' }); } next();} app.get('/api/public', (req, res) => res.json({ msg: 'للجميع' })); // الوسيط هنا يحمي هذا المسار فقطapp.get('/api/secret', requireKey, (req, res) => res.json({ msg: 'سرّي' })); app.listen(3000);
الطلب الثاني بلا مفتاح أوقفه الوسيط بالحالة 401 ولم يصل إلى المعالج. الثالث مرّ لأن المفتاح صحيح.
٤وسيطات جاهزة
كثير من الوسطاء مكتوب مسبقًا. استخدمنا منها express.json()، وهذه أشهر البقية:
| الوسيط | التثبيت | وظيفته |
|---|---|---|
express.json() | مدمج | يقرأ جسم JSON إلى req.body |
express.urlencoded() | مدمج | يقرأ بيانات النماذج HTML |
express.static('public') | مدمج | يقدّم ملفات ثابتة (HTML و CSS والصور) من مجلد |
cors() | npm i cors | يسمح لواجهة على نطاق آخر (مثل Vite على 5173) بطلب الخادم |
morgan('dev') | npm i morgan | يسجّل كل طلب في الطرفية |
helmet() | npm i helmet | يضيف ترويسات أمان (الوحدة 11) |
const express = require('express');const cors = require('cors');const morgan = require('morgan');const path = require('path');const app = express(); app.use(morgan('tiny'));app.use(cors({ origin: 'http://localhost:5173' }));app.use(express.static(path.join(__dirname, 'public'))); app.get('/api/time', (req, res) => res.json({ now: 'الآن' })); app.listen(3000);
<!DOCTYPE html><html lang="ar" dir="rtl"><head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head><body><h1>صفحة ثابتة من مجلد public</h1></body></html>
h1 { color: #149ECA; }الطلب الأول قدّمه express.static من الملف public/index.html دون أن نكتب مسارًا. وفي الثاني أضاف cors الترويسة Access-Control-Allow-Origin. أما morgan فطبع سطرًا لكل طلب في الطرفية مع زمن الاستجابة.
٥معالجة الأخطاء في مكان واحد
حين يرمي معالجٌ خطأً، يلتقطه Express ويرسل صفحة 500 عامة. الأفضل أن نكتب وسيط أخطاء واحدًا في آخر الملف يرد بـ JSON موحّد. علامته المميزة: أربعة معاملات (err, req, res, next). للوصول إليه من معالج عادي نستدعي next(err)، أما في الدوال async فيكفي أن نرمي الخطأ (ميزة في Express 5):
const express = require('express');const app = express(); app.get('/api/divide/:a/:b', (req, res, next) => { const a = Number(req.params.a), b = Number(req.params.b); if (b === 0) { const err = new Error('لا يمكن القسمة على صفر'); err.status = 400; return next(err); // اذهب إلى وسيط الأخطاء } res.json({ result: a / b });}); app.get('/api/crash', async (req, res) => { throw new Error('عطل في قاعدة البيانات'); // Express 5 يلتقطه}); // 404 لكل مسار لم يُعرَّف (بعد كل المسارات)app.use((req, res) => { res.status(404).json({ error: 'المسار غير موجود' });}); // وسيط الأخطاء: أربعة معاملات، وفي النهاية دائمًاapp.use((err, req, res, next) => { console.error(err.message); res.status(err.status || 500).json({ error: err.message });}); app.listen(3000);
كل الأخطاء صارت تصل إلى الواجهة بالشكل نفسه {"error": …}، فتعرضها React بسهولة. وطبع وسيط الأخطاء رسالته في الطرفية ليراها المطوّر.
٦مختبرك
أضف وسيطًا يقيس زمن كل طلب ويطبعه، ثم وسيطًا يرفض أي طلب POST لا يحمل الترويسة Content-Type: application/json.
توقّع وتحقّق
ماذا يحدث إن لم يستدعِ الوسيط next() ولم يرسل ردًّا؟
كيف يعرف Express أن الوسيط معالج أخطاء؟
أين نضع وسيط 404 العام؟
أي وسيط يسمح لواجهة على منفذ آخر بطلب الخادم؟
Cross-Origin.الخلاصة
- الوسيط
(req, res, next): يرد أو يستدعيnext. - الترتيب في الملف هو ترتيب التنفيذ.
- وسيط على مسار:
app.get(path, mw, handler). - وسيط الأخطاء بأربعة معاملات وفي النهاية؛ نصل إليه بـ
next(err)أو بالرمي فيasync. - 404 العام بعد كل المسارات.
تحدٍّ برمجي: بوابة الإدارة
متوسطخادم فيه مسارات عامة ومسارات للإدارة محمية بمفتاح، وأخطاء موحّدة.
- وسيط
loggerلكل الطلبات يطبع في الطرفيةMETHOD URL(مثلGET /api/news). - وسيط
adminOnlyعلى مسارGET /api/admin/statsفقط: إن لم تكن الترويسةx-roleتساويadminيرد بالحالة 403 و{"error": "للمشرفين فقط"}. - أي مسار غير معرَّف يرد بالحالة 404 و
{"error": "غير موجود"}. - وسيط أخطاء في النهاية يرد بالحالة 500 و
{"error": رسالة الخطأ}؛ وGET /api/failيرمي خطأً رسالته «تعطّل».
👀 اعرض الاستجابات المطلوبة
GET /api/newsGET /api/admin/statsGET /api/admin/statsGET /api/xyzGET /api/fail
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- الأخبار تعمل
- الإحصاءات بلا دور ← 403
- … ومعها
{"error": "للمشرفين فقط"} - مع
x-role: adminتصل الإحصاءات - مسار غير معرَّف ←
{"error": "غير موجود"} - … بالحالة 404
- الخطأ يصل بصيغة
JSON {"error": "تعطّل"} - الطرفية تُظهر سطرًا لكل طلب
💡 تلميح 1
💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express(); function logger(req, res, next) { console.log(req.method + ' ' + req.url); next();}function adminOnly(req, res, next) { if (req.headers['x-role'] !== 'admin') return res.status(403).json({ error: 'للمشرفين فقط' }); next();} app.use(logger);app.get('/api/news', (req, res) => res.json(['خبر 1', 'خبر 2']));app.get('/api/admin/stats', adminOnly, (req, res) => res.json({ users: 120 }));app.get('/api/fail', (req, res) => { throw new Error('تعطّل'); }); app.use((req, res) => res.status(404).json({ error: 'غير موجود' }));app.use((err, req, res, next) => res.status(500).json({ error: err.message })); app.listen(3000);