Middleware در Next.js به ما این امکان را میدهد که قبل از نهایی شدن یک درخواست، کدی را اجرا کنیم و پاسخ را تغییر دهیم. این قابلیت در کنار Edge Functions، ابزاری قدرتمند در اختیار توسعهدهندگان قرار میدهد تا به عملکرد بهتر و امکانات پیشرفتهتری دست پیدا کنند.
Middleware برای اولینبار در نسخه ۱۲ Next.js معرفی شد و در نسخههای بعدی بهصورت مداوم بهبود پیدا کرد. از نسخه ۱۳ به بعد، میتوانیم از Middleware برای پاسخ مستقیم به درخواستها استفاده کنیم، بدون اینکه الزاماً از route handler عبور کنیم. این موضوع میتواند هم از نظر performance و هم از نظر امنیت، تأثیر مثبتی داشته باشد. همچنین Middleware بهخوبی با Vercel Edge Functions کار میکند.
Edge Functionها به ما اجازه میدهند کد خود را در edge شبکه اجرا کنیم. به همین دلیل، در این مقاله بررسی میکنیم Middleware چگونه با Edge Functions کار میکند و چرا دانستن این موضوع اهمیت دارد.
Middleware در Next.js لایهای از کد است که امکان اجرای منطقهای مختلف را پیش از نهایی شدن یک درخواست فراهم میکند و به ما اجازه میدهد پاسخ را بر اساس شرایط گوناگون تغییر دهیم. این لایه عملاً بین درخواست ورودی و اپلیکیشن قرار میگیرد و کنترل بیشتری بر جریان request/response در اختیار توسعهدهنده قرار میدهد.
به کمک Middleware میتوان اقداماتی مانند احراز هویت، بررسی سطح دسترسی کاربران یا تغییر مسیر درخواستها را قبل از رسیدن به مقصد نهایی انجام داد. این قابلیت باعث میشود ساختار اپلیکیشن منسجمتر، منعطفتر و قابل مدیریتتر باشد.
Middleware در نسخه ۱۲ Next.js با هدف بهبود Router و بر اساس بازخورد توسعهدهندگان معرفی شد. در نسخههای بعدی، این قابلیت بهتدریج تکامل یافت و تجربه توسعهدهنده نیز بهبود پیدا کرد. در نهایت، در نسخه ۱۵، پشتیبانی از Node.js runtime اضافه شد تا پیادهسازی سناریوهای پیچیدهتر نیز امکانپذیر شود.
با استفاده از Middleware میتوان منطق سفارشی را بهصورت یکپارچه در مسیر پردازش درخواستها قرار داد و رفتار اپلیکیشن را متناسب با نیازهای خاص تنظیم کرد. این لایه امکان تغییر درخواستها و پاسخها را فراهم میکند؛ برای مثال میتوان هدرها را مدیریت کرد، ریدایرکتهای URL را پیادهسازی نمود یا درخواستهای ورودی و خروجی را مانیتور کرد. در مجموع، Middleware ابزاری انعطافپذیر است که میتواند امنیت، کارایی و قابلیتهای یک اپلیکیشن وب را به شکل محسوسی بهبود دهد.
برای درک بهتر نحوه عملکرد Middleware، بیایید یک Middleware ساده ایجاد کنیم. برای استفاده از Middleware، لازم است فایلی با نام middleware.js یا middleware.ts در همان سطحی که پوشه app قرار دارد، بسازیم.
برخلاف Next.js نسخه ۱۲ که نیاز به استفاده از underscore داشت، در نسخه ۱۵ دیگر نیازی به این کار نیست. این فایل شامل کدی خواهد بود که قبل از نهایی شدن درخواست اجرا میشود.
درون این فایل، کد زیر را قرار میدهیم:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// Restrict the /api/hello endpoint to only POST
if (pathname === "/api/hello") {
if (req.method !== "POST") {
return new NextResponse(
`Cannot access this endpoint with ${req.method}`,
{ status: 400 }
);
}
}
return NextResponse.next();
}
توابع Middleware نقش مهمی در پردازش درخواستها و پاسخها در یک اپلیکیشن وب دارند. Middleware با قطع کردن درخواستها و پاسخها، قبل از اینکه به هسته اصلی اپلیکیشن برسند، روی آنها اعمال میشود.
یکی از مهمترین کاربردهای Middleware، احراز هویت (Authentication) است. Middleware به ما این امکان را میدهد که قبل از دادن دسترسی به کاربر، هویت او را بررسی کنیم. با احراز هویت کاربران، اطمینان حاصل میکنیم که فقط افراد مجاز به منابع محافظت شده دسترسی دارند و در نتیجه، امنیت کلی اپلیکیشن افزایش پیدا میکند.
مجوزدهی (Authorization) نیز یکی دیگر از قابلیتهای کلیدی Middleware است. بعد از اینکه کاربران احراز هویت شدند، میتوانیم بر اساس نقشها یا سطح دسترسی آنها، کنترل دسترسی را پیادهسازی کنیم. به این معنا که برخی صفحات یا منابع اپلیکیشن فقط برای کاربران یا گروههای خاصی در دسترس خواهند بود.
علاوه بر این، Middleware میتواند از مکانیزمهای Caching برای بهینهسازی عملکرد اپلیکیشن استفاده کند. با ذخیره دادههایی که بهصورت مکرر استفاده میشوند، نیاز به دریافت مجدد آنها از دیتابیس کاهش پیدا میکند و سرعت پاسخدهی افزایش مییابد.
با این حال، Middleware برخلاف API Routes برای تولید خروجی HTML، رندر UI یا ارسال پاسخهای JSON طراحی نشده است. نقش اصلی آن، پردازش و هدایت درخواستها در مسیر اپلیکیشن است؛ تصمیمی که بهصورت هدفمند در طراحی Middleware اتخاذ شده و بهعنوان محدودیت در نظر گرفته نمیشود.
در ادامه، اقداماتی که Middleware میتواند انجام دهد را بررسی میکنیم.
با استفاده از NextResponse.next() میتوانیم قبل از اینکه Middleware به مقصد اصلی خود برسد، یک شرط یا بررسی خاص انجام دهیم. این بررسی میتواند شامل وجود توکن کاربر، دادههای مشخص یا هر منطق دلخواه دیگری باشد. اگر این شرط برقرار نباشد، میتوانیم جلوی ادامه جریان عادی Middleware را بگیریم.
بهعنوان مثال، در این سناریو بررسی میکنیم که آیا کاربر معتبر است یا خیر. در صورتی که کاربر معتبر باشد، فقط NextResponse.next() اجرا میشود و Middleware به کار خود ادامه میدهد:
if (isValidUser) {
return NextResponse.next();
}
این متد عملاً برای هدایت کاربر به یک URL دیگر، بر اساس شرایط مشخص استفاده میشود. بهعنوان مثال، اگر کاربر لاگین نکرده باشد، Middleware میتواند او را به صفحه ورود ریدایرکت کند. البته این رفتار را میتوان با منطق سمت کلاینت هم پیادهسازی کرد.
با این حال، این روش از نظر امنیتی چالشهایی به همراه دارد و معمولاً پیش از انجام ریدایرکت، محتوای صفحه برای لحظهای کوتاه به کاربر نمایش داده میشود. به همین دلیل، استفاده از Middleware در این سناریو انتخاب مناسبتری است:
if (!isLoggedIn) {
return NextResponse.redirect(new URL("/login", request.url));
}
این متد مسیر درخواست را بدون تغییر URL در مرورگر بازنویسی میکند. این قابلیت میتواند برای اهداف تست یا حتی برای سرو کردن محتوا از یک سرور یا مسیر متفاوت بسیار کاربردی باشد.
برای مثال، در این سناریو زبان (locale) کاربر بررسی میشود. اگر زبان کاربر آلمانی (de) باشد، مقدار de به مسیر اضافه میشود و محتوا به زبان آلمانی نمایش داده خواهد شد:
if (locale === "de") {
return NextResponse.rewrite(new URL("/fr" + request.nextUrl.pathname, request.url));
}
این متد در واقع یک constructor است که مانع ادامه پیدا کردن پردازش درخواست میشود. در این حالت، میتوانیم خروجیهایی مانند HTML، JSON یا plain text ارسال کنیم، با این محدودیت مهم که امکان ارسال محتوای server-rendered وجود ندارد:
if (isRateExceeded) {
return new NextResponse("Too many request, Please try after some time", { status: 429 });
}
Middleware به ما اجازه میدهد بدون اعمال تغییر مستقیم در کد اپلیکیشن، قابلیتهای جدیدی به آن اضافه کنیم. همین ویژگی باعث میشود بتوانیم فیچرهای تازه را پیادهسازی کنیم یا باگها را برطرف کنیم، آن هم بدون نیاز به دیپلوی نسخهی جدید اپلیکیشن. علاوه بر این، از این رویکرد میتوان برای مقیاسپذیری سیستم نیز استفاده کرد؛ بهگونهای که بخشی از پردازشها به سرورهای دیگر منتقل شوند.
در کنار این مزایا، استفاده از Middleware معایبی هم دارد. اضافه شدن یک لایهی میانی میتواند پیچیدگی اپلیکیشن را افزایش دهد و در نتیجه، فرآیند توسعه، دیپلوی و نگهداری را دشوارتر کند.
از طرف دیگر، استفاده از Middleware ممکن است هزینههایی به همراه داشته باشد؛ چرا که نیازمند میزبانی و نگهداری در یک محیط اجرایی جداگانه است. در مورد Next.js، این موضوع به معنای وابستگی بیشتر به اکوسیستم این فریمورک است. اگر در آینده تصمیم بگیریم از Next.js فاصله بگیریم و باندلهای جاوااسکریپت را بهصورت مستقل در محیط دیگری میزبانی کنیم، این وابستگی میتواند به یک گلوگاه تبدیل شود.
در نهایت، Middleware میتواند باعث ایجاد تأخیر در پاسخدهی اپلیکیشن شود، زیرا منطق آن باید قبل از پردازش نهایی درخواست اجرا شود. برای کاهش اثر این تأخیر، معمولاً از loaderها یا skeleton screenها استفاده میشود؛ راهکارهایی که در برخی سناریوها ممکن است تجربه کاربری ایدهآلی ایجاد نکنند.
در ادامه، چند سناریوی رایج را بررسی میکنیم که در آنها استفاده از Middleware در Next.js منطقی و کاربردی است.
Middleware در Next.js از طریق کلید geo در API مربوط به NextRequest، به اطلاعات جغرافیایی کاربر دسترسی میدهد. این قابلیت به ما اجازه میدهد محتوای وابسته به موقعیت مکانی کاربر را نمایش دهیم.
برای مثال، اگر در حال توسعه وبسایتی برای یک شرکت فروش کفش با شعب مختلف در مناطق گوناگون باشیم، میتوانیم بر اساس موقعیت کاربر، کفشهای ترند یا پیشنهادهای ویژه همان منطقه را نمایش دهیم.
با استفاده از کلید cookies در API مربوط به NextRequest، میتوانیم کوکیها را تنظیم نماییم و کاربران را احراز هویت کنیم. همچنین میتوانیم با مسدود کردن رباتها، کاربران یا حتی مناطق جغرافیایی خاص، امنیت سایت را افزایش دهیم.
در چنین حالتی، با بازنویسی درخواست، میتوان کاربران مسدود شده را به یک صفحه بلاک شده هدایت کرد یا حتی یک خطای 404 نمایش داد.
A/B Testing به فرآیندی گفته میشود که در آن نسخههای متفاوتی از یک وبسایت به کاربران نمایش داده میشود تا مشخص شود کدام نسخه بازخورد بهتری دارد. در گذشته، این کار معمولاً در سمت کلاینت و روی سایتهای استاتیک انجام میشد که نتیجه آن، پردازش کندتر و احتمال ایجاد layout shift بود.
اما Middleware در Next.js این امکان را فراهم میکند که پردازش درخواستها در سمت سرور انجام شود. این موضوع هم سرعت را افزایش میدهد و هم از ایجاد layout shift جلوگیری میکند. در A/B Testing با Middleware، معمولاً از کوکیها برای اختصاص کاربران به گروههای مختلف (bucket) استفاده میشود و سپس سرور، کاربر را بر اساس bucket مربوطه به نسخه A یا B هدایت میکند.
با استفاده از Middleware میتوانیم از سوءاستفادهها جلوگیری کنیم و یک Rate Limiter ساده پیادهسازی کنیم. Middleware در Next.js این امکان را فراهم میکند که بدون نیاز به بکاند سنتی یا دیتابیس، یک Rate Limiter سبک و پایه داشته باشیم.
این Rate Limiter میتواند بر اساس IP، هدرها و سایر پارامترها شخصیسازی شود. در پشت صحنه، Middleware درخواستهای ارسالی از یک IP مشخص را بررسی میکند و اگر تعداد درخواستها از یک حد تعیین شده بیشتر شود، درخواستهای بعدی مسدود خواهند شد.
در کدی که بالاتر اشاره شد، ابتدا بررسی میشود که آیا URL درخواست با الگوی /api/hello مطابقت دارد یا خیر. اگر مطابقت داشته باشد، متد درخواست بررسی میشود. اگر متد POST نباشد، یک پاسخ 400 Bad Request به همراه پیام خطا که شامل متد درخواست است، برگردانده میشود.
در صورتی که متد درخواست POST باشد، تابع NextResponse.next() فراخوانی میشود تا Next.js پردازش درخواست را ادامه دهد.
حالا که درک مناسبی از Middleware پیدا کردهایم، منطقی است که درباره Edge Functions صحبت کنیم؛ مفهومی که ارتباط عمیق و مستقیمی با Middleware دارد. در ادامه، یک مرور سریع روی Edge Functions و نقشی که در بهبود عملکرد اپلیکیشن ایفا میکنند خواهیم داشت.
اگر تا به حال با Serverless Functions کار کرده باشیم، بهخوبی میتوانیم اهمیت Edge Functions را درک کنیم. برای فهم بهتر این مفهوم، Edge Functions را با Serverless Functions مقایسه میکنیم.
وقتی یک Serverless Function را روی Vercel دیپلوی میکنیم، این تابع روی یک سرور مشخص در نقطهای از جهان مستقر میشود. در نتیجه، هر درخواستی که به این تابع ارسال شود، دقیقاً در همان مکانی که سرور قرار دارد اجرا خواهد شد.
اگر درخواست از موقعیتی نزدیک به آن سرور ارسال شود، پاسخ بسیار سریع خواهد بود. اما اگر کاربر در فاصله جغرافیایی دوری قرار داشته باشد، زمان پاسخدهی بهطور محسوسی افزایش پیدا میکند. این دقیقاً همان جایی است که Edge Functions اهمیت پیدا میکنند. Edge Functions در واقع نوعی Serverless Function هستند که از نظر جغرافیایی در نزدیکی کاربر اجرا میشوند و باعث میشوند درخواستها، بدون توجه به موقعیت مکانی کاربر، با سرعت بسیار بالایی پردازش شوند.
هنگامی که یک اپلیکیشن Next.js را روی Vercel دیپلوی میکنیم، Middleware بهصورت Edge Functions در تمام ریجنهای جهان مستقر میشود. این یعنی بهجای اینکه یک تابع روی یک سرور واحد اجرا شود، همان تابع روی چندین سرور در نقاط مختلف دنیا در دسترس خواهد بود. در این معماری، Edge Functions مستقیماً از Middleware استفاده میکنند.
یکی از ویژگیهای منحصربهفرد Edge Functions این است که در مقایسه با Serverless Functions معمولی، حجم بسیار کمتری دارند. علاوه بر این، Edge Functions روی V8 runtime اجرا میشوند که باعث میشود عملکرد آنها تا حدود ۱۰۰ برابر سریعتر از اجرای Node.js در کانتینرها یا ماشینهای مجازی باشد.
برای ساخت Edge Functions، ابتدا یک API Route جدید ایجاد میکنیم. هرچند Next.js بهصورت پیشفرض یک API Route نمونه با نام hello.js در اختیار ما قرار میدهد، اما در این مثال قصد داریم یک مسیر جدید بسازیم.
برای این کار، یک فایل جدید مستقیماً داخل پوشه pages/api پروژه ایجاد میکنیم و کد زیر را در آن قرار میدهیم:
import { NextResponse } from 'next/server';
export const config = {
runtime: 'edge', //This specifies the runtime environment that the middleware function will be executed in.
};
export default (request) => {
return NextResponse.json({
name: `Hello, from ${request.url} I'm now an Edge Function!`,
});
};
پس از آن، میتوانیم پروژه را روی Vercel یا هر پلتفرم Edge Computing دیگری که ترجیح میدهیم، دیپلوی کنیم.
درک درست نحوه استفاده همزمان از Middleware و Edge Functions، به ما کمک میکند مشکل اشتراکگذاری یک سیستم لاگین مشترک بین چند اپلیکیشن مختلف را بهصورت بهینه حل کنیم؛ موضوعی که معمولاً در سناریوهایی مثل احراز هویت، محافظت در برابر باتها، ریدایرکتها، پشتیبانی مرورگرها، Feature Flagها، A/B Testing، آنالیتیکس سمت سرور، لاگگیری و Geolocation دیده میشود.
با استفاده از Middleware، اجرای Edge Functions به شکل قابل توجهی سریعتر میشود؛ چرا که پردازش درخواستها با حداقل تأخیر انجام میشود و هزینه راهاندازی اولیه توابع (Cold Start) عملاً کاهش مییابد. در نتیجه، زمان پاسخدهی به کاربر بهطور محسوسی بهبود پیدا میکند.
با استفاده از Edge Functions میتوانیم فایلها و منطق اپلیکیشن را در چندین موقعیت جغرافیایی اجرا کنیم. در این حالت، نزدیکترین ریجن به کاربر، پاسخ درخواست او را ارسال میکند و همین موضوع باعث میشود سرعت پاسخدهی مستقل از موقعیت جغرافیایی کاربر باشد.
بهصورت سنتی، برای افزایش سرعت، محتوای وب از طریق CDN به کاربران ارائه میشود. اما از آنجا که این محتوا استاتیک است، قابلیت ارائه محتوای داینامیک را از دست میدهیم. از طرف دیگر، با استفاده از Server-Side Rendering میتوانیم محتوای داینامیک داشته باشیم، اما بخشی از سرعت قربانی میشود.
در مقابل، زمانی که Middleware را مانند CDN روی Edge دیپلوی میکنیم، منطق سمت سرور را به مبدأ کاربران نزدیکتر میکنیم. نتیجه این کار، ترکیبی از سرعت بالا و شخصیسازی محتوا برای کاربران است. بهعنوان توسعهدهنده، حالا میتوانیم وبسایت خود را بسازیم و دیپلوی کنیم و سپس خروجی آن را در CDNهای سراسر جهان کش کنیم.
در این بخش بررسی میکنیم که چگونه میتوانیم Middleware و Edge Functions را در کنار هم استفاده کنیم. هدف ما این است که با استفاده از Middleware، یک endpoint محافظت شده برای Edge Functions ایجاد کنیم.
ابتدا ترمینال را باز میکنیم و پوشهای برای پروژه میسازیم:
mkdir next_middleware
سپس وارد پوشه ساخته شده میشویم و Next.js را با دستور زیر نصب میکنیم:
npx create-next-app@latest
در طول فرآیند نصب، نام پروژه و سایر تنظیمات دلخواه را انتخاب میکنیم و مطمئن میشویم که App Router را انتخاب کردهایم تا از جدیدترین قابلیتهای Next.js استفاده کنیم.
پس از آن، Middleware را ایجاد میکنیم. یک فایل با نام middleware.js در root پروژه (در کنار پوشه app) میسازیم و کد زیر را داخل آن قرار میدهیم:
// middleware.js
import { NextResponse } from "next/server";
import { NextRequest } from "next/server";
export function middleware(request) {
// Get the admin cookie id from the request.
let adminCookieId = request.cookies.get("adminCookieId")?.value;
// If the admin cookie id is not "abcdefg", redirect the user to the admin login page.
if (adminCookieId !== "abcdefg") {
return NextResponse.redirect(new URL("/admin-login", request.url));
}
}
export const config = {
matcher: "/api/protected/:path*",//The matcher specifies the URL patterns that this middleware will be applied to.
};
در کد بالا، فایل middleware.js یک تابع Middleware تعریف میکند که برای محافظت از یک API Endpoint در Next.js استفاده میشود. این Middleware درخواست را بررسی میکند تا ببیند آیا مقدار adminCookieId وجود دارد یا خیر. اگر این مقدار وجود نداشته باشد یا برابر با "abcdefg" نباشد، کاربر به صفحه لاگین ادمین ریدایرکت میشود.
در ادامه، یک endpoint محافظت شده ایجاد میکنیم. داخل پوشه pages/api، یک پوشه با نام protected میسازیم و سپس یک فایل index.js داخل آن قرار میدهیم و کد زیر را در آن مینویسیم:
// do this only if you are using the pages folder, this has been deprecated in the app router
//export const config = {
// runtime: "edge",
// };
export default function handler(req, res) {
const { language } = req.query;
// Personalization logic based on user preferences
let greeting;
if (language === "en") {
greeting = "Hello! Welcome!";
} else if (language === "fr") {
greeting = "Bonjour! Bienvenue!";
} else if (language === "es") {
greeting = "¡Hola! ¡Bienvenido!";
} else {
greeting = "Welcome!";
}
res.status(200).json({ greeting });
}
اگر از app router استفاده میکنیم، میتوانیم این منطق را به مسیر app/api/protected/route.js منتقل کنیم و کد زیر را در آن قرار دهیم:
export const runtime = 'edge'; // Run this API route at the edge using edge function
export async function GET(request) {
// getting the slug using searchParam as app router is a server rendered component
const { searchParams } = new URL(request.url);
const language = searchParams.get('language');
// Personalization logic based on query parameter
let greeting;
switch (language) {
case 'en':
greeting = 'Hello! Welcome!';
break;
case 'fr':
greeting = 'Bonjour! Bienvenue!';
break;
case 'es':
greeting = '¡Hola! ¡Bienvenido!';
break;
default:
greeting = 'Welcome!';
}
return new Response(JSON.stringify({ greeting }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
}
کد بالا یک Edge Function را تعریف میکند که میتواند پیام خوشامدگویی را بر اساس زبان ترجیحی کاربر شخصیسازی کند. این تابع زبان کاربر را از Query Parameterهای درخواست دریافت میکند، بر اساس آن پیام مناسب را انتخاب میکند و در نهایت، پاسخ را بهصورت JSON برمیگرداند.
در بخش config مشخص میکنیم که این تابع باید روی Edge اجرا شود. این یعنی تابع در نزدیکترین موقعیت جغرافیایی به کاربر اجرا میشود و در نتیجه، عملکرد و سرعت پاسخدهی بهطور قابل توجهی بهبود پیدا میکند.
در این مقاله، بررسی کردیم که Middleware چیست، چگونه کار میکند و چه مزایا و معایبی دارد. همچنین تلاش کردیم با نگاهی دقیقتر، پیامدها و محدودیتهای آن را نیز بهتر درک کنیم.
در ادامه، بهصورت خلاصه به Edge Functions پرداختیم؛ کاربردهای آنها را مرور کردیم و با نحوه پیادهسازی یک Edge Function ساده آشنا شدیم. این توابع نقش مهمی در اجرای منطق اپلیکیشن در نزدیکترین موقعیت به کاربر دارند و تأثیر مستقیمی بر بهبود سرعت و تجربه کاربری میگذارند.
Middleware با توانایی مدیریت مسائل پایهای مانند احراز هویت و Geolocation، یکی از قابلیتهای کلیدی Next.js محسوب میشود. این لایه به ما اجازه میدهد منطق مورد نیاز را پیش از پردازش نهایی درخواست اجرا کنیم و رفتار اپلیکیشن را با حداقل سربار عملکردی کنترل کنیم؛ رویکردی که در بسیاری از سناریوها عملکردی نزدیک به اپلیکیشنهای وب استاتیک ارائه میدهد.