میانافزارها در Express را عمیق یاد بگیرید: چرا ترتیب اجرا، سرنوشت API شما را تعیین میکند؟
چرا بیشتر باگهای امنیتی و منطقی در APIهای Express از ترتیب اشتباه میانافزارها ناشی میشود و چطور با درک دقیق چرخهٔ اجرا، یک زنجیرهٔ میدلور قابلاعتماد بسازیم؟ راهنمای فنی و کاربردی.
میانافزارها در Express را عمیق یاد بگیرید یعنی چیزی بیش از نوشتن چند تابع (req, res, next)؛ در تجربهای که از بازبینی دهها پروژهٔ Express داشتهام، بیشتر باگهای امنیتی و منطقی این فریمورک ریشه در «ترتیب اجرای» میانافزارها داشته، نه در خودِ کد هر میدلور. Middleware در Express.js یک قرارداد ساده است که در ظاهر ساده به نظر میرسد، ولی وقتی تعداد میدلورها از پنج بیشتر میشود، رفتار نهایی به یک سیستم پیچیده تبدیل میشود که فقط با درک دقیق چرخهٔ درخواست قابل کنترل است.
میدلور دقیقاً چیست؟
Middleware یک تابع است که در مسیر پردازش درخواست HTTP، بین ورود درخواست به سرور و ارسال پاسخ نهایی اجرا میشود. تصویر ذهنی دقیق این است: هر درخواست مانند یک بستهٔ پستی است که از چند مرحلهٔ بازرسی عبور میکند؛ هر مرحله میتواند بسته را باز کند، چیزی به آن اضافه کند، آن را رد کند، یا در صورت مشکل، آن را برگرداند. در Express این مراحل همان میدلورها هستند و به ترتیبی که در کد ثبت شدهاند، اجرا میشوند.
برخلاف بسیاری از فریمورکهای opinionated که رفتار این مراحل را پنهان میکنند، Express آن را شفاف و در اختیار توسعهدهندهٔ شما قرار میدهد. همین شفافیت، نقطهٔ قوت آن است — و در عین حال، دلیل اصلی مسئولیت سنگین توسعهدهنده است. برای درک بهتر جایگاه این مفهوم در معماری کلی، «معماری وب چیست» تصویر بزرگتری میدهد.
میدلور، یک قطعهٔ کد نیست؛ یک تصمیم معماری است که در قالب یک تابع کوچک بیان میشود.
امضای تابع و پارامتر next
میدلور در Express یک تابع با امضای استاندارد (req, res, next) است. هر پارامتر نقش مشخصی دارد:
function myMiddleware(req, res, next) {
// req: درخواست HTTP
// res: پاسخ HTTP
// next: تابعی که اجرای میدلور بعدی را فعال میکند
next();
}
سه رفتار ممکن در هر میدلور وجود دارد:
- ادامه دادن زنجیره: با فراخوانی
next(). - پایان دادن زنجیره: با ارسال پاسخ (
res.send,res.json,res.end). - عبور دادن خطا: با
next(err)که مستقیماً میدلور خطا را فعال میکند.
اگر هیچکدام از این سه انجام نشود، درخواست در هوا معلق میماند و کاربر پاسخی دریافت نمیکند؛ این حالت، منبع بسیاری از باگهای سخت در پروژههای واقعی است.
انواع میدلور در Express
Express پنج نوع میدلور را میشناسد که تشخیص درست جایگاه هرکدام، نیمی از مسیر موفقیت است:
| نوع | نحوهٔ ثبت | کاربرد معمول |
|---|---|---|
| Application-level | app.use() یا app.METHOD() | لاگ، پارس بدنه، احراز هویت سراسری |
| Router-level | router.use() | میدلور مختص یک گروه مسیر |
| Error-handling | app.use((err, req, res, next) => {}) | مدیریت متمرکز خطا |
| Built-in | express.json(), express.static() | پارس بدنه، سرویس فایل استاتیک |
| Third-party | app.use(helmet()) | امنیت، CORS، محدودسازی نرخ |
تصمیم درست بین این پنج نوع، همان چیزی است که در پروژههای حرفهای تفاوت محسوسی ایجاد میکند. ساخت یک API با این ساختار را در «ساخت API سریع با Node.js و Express» گامبهگام توضیح دادهام.
ترتیب اجرا: قاعدهای که همهچیز را تعیین میکند
در Express، ترتیب ثبت میدلورها معادل ترتیب اجرای آنها است. اگر میدلور احراز هویت را بعد از میدلور پاسخدهی ثبت کنید، عملاً هیچوقت اجرا نمیشود. اگر میدلور پارس بدنه را بعد از کنترلر قرار دهید، req.body در کنترلر undefined خواهد بود.
یک الگوی پایه که تقریباً همیشه درست است:
app.use(helmet()); // 1. امنیت
app.use(cors()); // 2. CORS
app.use(express.json({ limit: "1mb" })); // 3. پارس بدنه
app.use(logger); // 4. لاگ
app.use(rateLimiter); // 5. محدودسازی نرخ
app.use("/api", apiRouter); // 6. مسیرهای اپلیکیشن
app.use(errorHandler); // 7. مدیریت خطا (همیشه آخر)
سه اشتباه پرتکرار در این بخش:
- قرار دادن میدلور خطا در میان سایر میدلورها — میدلور خطا باید همیشه آخرین باشد.
- ثبت میدلور احراز هویت در سطح application بهجای router — که باعث میشود روی routeهای عمومی هم اعمال شود.
- نادیده گرفتن ترتیب در مسیرهای تودرتو — که در پروژههای بزرگ منبع سردرگمی است.
برای درک مفهوم مسیر در Express و جایگاه میدلور، «Express.js: مینیمال اما قدرتمند» را بخوانید.
زنجیرهٔ میدلور و کنترل جریان
زنجیرهٔ میدلور را مثل یک لولهٔ آب در نظر بگیرید: هر میدلور یک شیر است که میتواند آب را رد کند، متوقف کند یا آن را با فشار بیشتری عبور دهد. در عمل، سه الگوی متداول کنترل جریان وجود دارد:
- زنجیرهٔ خطی: هر میدلور یک مرحله است و همه باید اجرا شوند.
- زنجیرهٔ شرطی: میدلور بر اساس شرطی (مثلاً وجود هدر) تصمیم میگیرد زنجیره را ادامه دهد یا پاسخ دهد.
- زنجیرهٔ شاخهای: از یک میدلور، کنترل به یکی از چند مسیر مختلف میرود (کاربری با
req.user.role).
نمونهٔ زنجیرهٔ شرطی برای احراز هویت با دو روش (توکن و API key):
function auth(req, res, next) {
const token = req.headers.authorization;
const apiKey = req.headers["x-api-key"];
if (token) return authByToken(token, req, res, next);
if (apiKey) return authByApiKey(apiKey, req, res, next);
return res.status(401).json({ error: "No credentials" });
}
الگوهای احراز هویت را در «احراز هویت در API» و پیادهسازی JWT را در «JWT چیست» بررسی کردهام.
میدلور خطا و تفاوت بنیادی آن
میدلور خطا در Express با بقیه فرق دارد: امضایش چهار پارامتر است.
function errorHandler(err, req, res, next) {
const status = err.status || 500;
const message = err.message || "Internal Server Error";
console.error(`[ERROR] ${req.method} ${req.url}:`, err);
res.status(status).json({
error: message,
...(process.env.NODE_ENV === "development" && { stack: err.stack })
});
}
سه نکتهٔ حیاتی در این میدلور:
- پارامتر اول آن (err) اجباری است و اگر حذف شود، Express آن را بهعنوان میدلور عادی میشناسد، نه خطا.
- میدلور خطا باید پس از همهٔ routeها و میدلورهای دیگر ثبت شود؛ در غیر این صورت هرگز اجرا نمیشود.
- هرگز اطلاعات حساس (stack trace، پیام دیتابیس) را در محیط تولید به کاربر برنگردانید.
این نکات در ظاهر ساده هستند ولی بیشتر باگهای امنیتی واقعی از نادیده گرفتن همین موارد ناشی میشوند. اگر میخواهید مبانی خطا در جاوااسکریپت را عمیقتر بشناسید، «مدیریت خطا در جاوااسکریپت» پیشنیاز مستقیم این بخش است.
میدلور خطا در Express، آخرین خط دفاع است؛ ولی اگر ترتیبش را اشتباه بچینید، این خط دفاع در عمل وجود ندارد.
میدلور در سطح Router
در پروژههای واقعی، بخش بزرگی از میدلورها باید فقط روی یک گروه مسیر خاص اجرا شوند، نه روی همه. Express برای این کار دو ابزار فراهم میکند:
const router = Router();
router.use(authMiddleware);
router.use("/admin", adminMiddleware);
router.get("/profile", profileController);
router.get("/admin/users", usersController);
app.use("/api/v1", router);
این الگو، دو مزیت مستقیم دارد: اول، سطح اجرا محدود میشود و بار اضافی روی routeهای عمومی تحمیل نمیشود. دوم، کد خوانا و ماژولار میماند. اصول کامل لایهبندی مسیرها را در «REST API در عمل» توضیح دادهام؛ طراحی درست مسیرها بدون درک این الگو کامل نیست.
میدلورهای بومی و پارتیهای محبوب
Express چند میدلور بومی دارد که در پروژههای واقعی جزو ضروریها هستند:
express.json()— پارس بدنهٔ JSON. همیشه باlimitمشخص استفاده کنید.express.urlencoded()— پارس فرمهای HTML.express.static()— سرویسدهی فایلهای استاتیک با پیکربندی cache.express.Router()— نه یک میدلور، بلکه واحد سازماندهی میدلورها و مسیرها.
در کنار اینها، چند پکیج جانبی تقریباً در هر پروژهٔ جدی حضور دارند:
helmet— تنظیم هدرهای امنیتی HTTP.cors— مدیریت سیاست دسترسی متقاطع.express-rate-limit— محدودسازی نرخ درخواست.compression— فشردهسازی gzip/brotli روی پاسخها.morganیاpino-http— لاگ ساختاریافتهٔ درخواستها.
جزئیات امنیتی کاربرد این میدلورها در «امنیت API» بررسی شده و راهاندازی CI/CD برای اطمینان از صحت پیکربندی در «راهاندازی CI/CD برای پروژههای کوچک» آمده است.
میدلور async و دام async/await
یکی از رایجترین اشتباهات در Express این است که میدلور async بدون مدیریت خطا نوشته شود. اگر پرامیس داخلی این تابع reject شود، Express آن را نمیگیرد و میدلور خطا هرگز فعال نمیشود. Express 5 این رفتار را بهبود داده، ولی در نسخهٔ ۴ این مسئله جدی است.
راهحل استاندارد، بستهبندی میدلور در یک تابع کوچک است:
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
router.get("/users", asyncHandler(async (req, res) => {
const users = await db.users.findAll();
res.json(users);
}));
این الگو، یکی از پرکاربردترین ترفندهای پروژههای Express حرفهای است. آشنایی با مبانی پرامیسها در «Promise در جاوااسکریپت» و «async/await در جاوااسکریپت» پیشنیاز درک این بخش است.
الگوهای حرفهای در پروژههای واقعی
چند الگویی که در پروژههای بالغ دیدهام و ارزش پیادهسازی دارند:
- Request ID: هر درخواست یک شناسهٔ یکتا بگیرد و در لاگها و پاسخها بیاید — برای ردیابی در محیط تولید حیاتی است.
- Tenant context: در اپهای چندمستأجری، میدلوری که پس از احراز هویت،
req.tenantرا ست کند. - Correlation context: برای معماریهای میکروسرویس، تشخیص ترتیب میدلورها بر اساس context فراهم شود.
- Validation pipeline: ترکیب اعتبارسنجی + پاکسازی + نگاشت داده در یک زنجیرهٔ کوتاه میدلور.
- Feature flags: فعال یا غیرفعالکردن رفتار API بدون تغییر کد.
پروژههایی که این الگوها را زودتر پیاده میکنند، در ماه ششم بهمراتب سریعتر از پروژههای بدون ساختار، قابلیت جدید اضافه میکنند. برای دیدن نقش کلی این الگوها در معماری، «بکاند چیست و چه وظایفی دارد» را مطالعه کنید.
پرتکرارترین خطاها در کار با میدلور
فهرست کوتاهی از اشتباهاتی که در پروژههای بازبینیشده دیدهام:
- فراخوانی
next()بعد از ارسال پاسخ — که باعث اجرای میدلورهای بعدی میشود. - فراموش کردن
next()در میدلور شرطی — که درخواست را معلق نگه میدارد. - ثبت میدلور خطا در میان میدلورهای عادی.
- نبود ترتیب مشخص در پروژههای تیمی — هر توسعهدهندهٔ جدید، یک میدلور در جای اشتباه اضافه میکند.
- استفاده از
app.useبرای منطق کسبوکار، بهجای کنترلر. - میدلورهای جامع که در همهٔ مسیرها اجرا میشوند، ولی فقط در چند route معنا دارند.
این خطاها در ظاهر کوچک بهنظر میرسند، ولی در محیط تولید میتوانند هزینهٔ سنگینی داشته باشند. اگر با مدیریت خطا در جاوااسکریپت آشنا نیستید، «مدیریت خطا در جاوااسکریپت» پیشنیاز جدی است.
پرسشهای پرتکرار درباره میدلور Express
آیا میدلور Express فقط برای احراز هویت است؟ نه؛ میدلور ابزار عمومی مدیریت درخواست است و در لاگگیری، CORS، محدودسازی نرخ، فشردهسازی، اعتبارسنجی و مدیریت خطا کاربرد دارد. محدودیت در تصور ما است، نه در خود Express.
آیا میتوان میدلور را در زمان اجرا حذف کرد؟ خیر، بهشکل استاندارد نمیتوان میدلور ثبتشده را حذف کرد. اگر نیاز به رفتار شرطی دارید، باید در داخل میدلور تصمیم بگیرید یا از چندین Router استفاده کنید. این محدودیت را در طراحی اولیه در نظر بگیرید.
چطور از اجرای دوبارهٔ میدلور جلوگیری کنم؟ با اطمینان از اینکه هر میدلور فقط یکبار در زنجیره ثبت میشود. اگر میدلور از یک router والد به ارث رسیده و در router فرزند هم ثبت شود، دوبار اجرا خواهد شد.
آیا میدلورها روی فایلهای استاتیک هم اعمال میشوند؟ بستگی به ترتیب ثبت دارد. اگر express.static قبل از میدلور شما ثبت شده باشد، میدلور شما برای فایلهای استاتیک اجرا نمیشود — چون درخواست در همان مرحله پاسخ داده میشود.
آیا Express میدلور خطا را در پروژههای بزرگ بهطور پیشفرض دارد؟ خیر، باید خودتان پیاده کنید. اگر تعداد مسیرها زیاد است، میدلور خطا با شناسهٔ یکتا برای پیگیری، ضروری میشود.
چطور ترتیب میدلورها را در پروژههای بزرگ مستند کنیم؟ با یک فایل مرکزی که ترتیب را بهصورت صریح ثبت میکند و در کد کامنت توضیحی میگذارد. اگر تیم بزرگ است، یک فایل middleware-order.md در ریشهٔ پروژه کمک بزرگی است.
آیا میدلور async را میتوان بدون بستهبندی نوشت؟ در Express 5 بله، ولی در نسخههای ۴ و قبلتر باید با asyncHandler بستهبندی شود. توصیهٔ من: همیشه بستهبندی کنید تا کد به نسخه گره نخورد.
آیا میدلورها روی همهٔ متدهای HTTP اجرا میشوند؟ اگر با app.use یا router.use ثبت شوند، بله. اگر با app.get یا app.post ثبت شوند، فقط برای متد مشخص اجرا میشوند.
چطور یک میدلور را فقط برای یک گروه از مسیرها فعال کنم؟ با router.use("/path", middleware) یا با ثبت میدلور در router اختصاصی و app.use("/path", router). این الگو بهترین راه محدودسازی سطح اجرا است.
آیا میدلورها میتوانند با هم تعامل داشته باشند؟ بله، از طریق مقادیر اضافهشده به req (مثلاً req.user). این الگو در پروژههای حرفهای رایج است ولی نیازمند مستندسازی دقیق برای جلوگیری از وابستگیهای پنهان.
نکات نهایی درباره معماری میدلور
میدلور در Express یک ابزار است، نه یک هدف. کاربرد درست آن، جداسازی مسئولیتهای متقاطع (cross-cutting concerns) است: احراز هویت، لاگ، مدیریت خطا، محدودسازی نرخ، و اعتبارسنجی. هر چیزی که منطق اصلی کسبوکار است، باید در لایهٔ کنترلر یا سرویس بماند، نه در میدلور.
سه اصل که در پروژههای موفق دیدهام:
- هر میدلور یک مسئولیت: اگر یک میدلور بیشتر از یک کار انجام میدهد، آن را به دو میدلور تقسیم کنید.
- ترتیب صریح و مستند: ترتیب ثبت میدلورها نباید نتیجهٔ تصادف باشد؛ باید انتخاب باشد.
- تست میدلورها در انزوا: هر میدلور باید جداگانه تست شود تا رفتار در ترکیب با بقیه قابل پیشبینی باشد.
میدلور در Express بههمین دلیل قدرتمند است که ساده است. ولی همین سادگی، همهٔ مسئولیت را به توسعهدهنده منتقل میکند؛ و همینجاست که تفاوت بین یک API آماتور و یک API حرفهای مشخص میشود. اگر در پروژهتان به الگویی رسیدهاید که در این مقاله جا نیفتاده — بهخصوص در زمینهٔ ترتیب میدلور در اپهای چندمستأجری یا edge computing — تجربهتان را بنویسید؛ همان جزئیات برای خوانندههای بعدی ارزشمند است.