راهنمای جامع Middleware در Next.js 15: از احراز هویت تا Rate Limiting و A/B Testing

در این راهنما ۵ الگوی عملی Middleware در Next.js 15 رو با کد کامل یاد می‌گیرید: محافظت از مسیرها با JWT، محدودسازی نرخ با Redis، هدرهای امنیتی، مسیریابی چندزبانه و تست A/B روی Edge — همراه بهترین شیوه‌ها و خطاهای رایج.

مقدمه: چرا Middleware مهم‌ترین ابزار نادیده‌گرفته‌شده Next.js هست؟

اگه مقاله‌های قبلی ما رو دنبال کرده باشید، تا الان با احراز هویت Auth.js v5، واکشی داده و کشینگ و عملیات CRUD با Drizzle ORM آشنا شدید. ولی یه بخش کلیدی هست که تقریباً تو همه این موضوعات نقش داره و — صادقانه بگم — شاید بهش اون‌قدر که باید توجه نشده: Middleware.

Middleware تو Next.js مثل یه نگهبان هوشمند عمل می‌کنه. قبل از رسیدن هر درخواست به صفحات و API‌های شما اجرا میشه و می‌تونه کاربر رو ریدایرکت کنه، هدرهای امنیتی اضافه کنه، نرخ درخواست‌ها رو محدود کنه، تست A/B اجرا کنه و کلی کار دیگه.

نکته جالبش اینه که Middleware روی Edge Runtime اجرا میشه — یعنی نزدیک‌ترین نقطه جغرافیایی به کاربر. تأخیر بسیار کم (معمولاً زیر ۳۰ میلی‌ثانیه) و تجربه کاربری فوق‌العاده سریع.

خب، تو این راهنما قراره ۵ الگوی عملی Middleware رو با کد کامل پیاده‌سازی کنیم:

  1. محافظت از مسیرها با بررسی احراز هویت
  2. محدودسازی نرخ (Rate Limiting) برای جلوگیری از سوءاستفاده
  3. هدرهای امنیتی برای مقابله با حملات رایج
  4. مسیریابی چندزبانه (i18n) بر اساس زبان مرورگر
  5. تست A/B بدون جاوااسکریپت سمت کلاینت

در نهایت هم یاد می‌گیریم چطور همه اینها رو در یک فایل Middleware سازمان‌دهی‌شده ترکیب کنیم. بزن بریم!

Edge Runtime چیه و چرا Middleware روش اجرا میشه؟

قبل از اینکه بریم سراغ کد، باید بفهمیم Middleware کجا و چطور اجرا میشه. باور کنید این درک باعث میشه خیلی از محدودیت‌ها و بهترین شیوه‌ها برامون منطقی بشه.

تفاوت Edge Runtime با Node.js Runtime

Edge Runtime یه محیط اجرای سبک‌وزن مبتنی بر Web API هست — نه Node.js کامل. یعنی:

  • سبک و سریع: Cold start تقریباً آنی (معمولاً زیر ۵۰ میلی‌ثانیه)
  • توزیع جهانی: کد شما روی نزدیک‌ترین سرور به کاربر اجرا میشه
  • بدون دسترسی به API‌های Node.js: ماژول‌هایی مثل fs، path و crypto (نسخه Node) در دسترس نیستن
  • Web Crypto API: به جای crypto نود، از Web Crypto API استفاده می‌کنید
  • بدون حالت (Stateless): نمی‌تونید state بین درخواست‌ها نگه دارید

این محدودیت‌ها ممکنه اول کمی آزاردهنده به نظر برسن. ولی در عوض سرعت و توزیع جغرافیایی فوق‌العاده‌ای به دست میارید. به هر حال Middleware قراره سبک و سریع باشه — نه اینکه کوئری سنگین دیتابیس اجرا کنه.

ترتیب اجرای درخواست‌ها در Next.js

وقتی یه درخواست به اپلیکیشن Next.js شما میاد، ترتیب پردازش به این شکله:

  1. headers (از next.config.js)
  2. redirects (از next.config.js)
  3. Middleware ← اینجا کد شما اجرا میشه
  4. beforeFiles rewrites
  5. فایل‌های استاتیک و صفحات

همون‌طور که می‌بینید، Middleware قبل از رسیدن درخواست به هر صفحه یا API Route اجرا میشه. عملاً اولین فرصت واقعی شما برای بررسی و دستکاری درخواست هست.

راه‌اندازی اولیه Middleware

ایجاد Middleware تو Next.js واقعاً ساده‌ست. کافیه یه فایل middleware.ts در ریشه پروژه (یا داخل پوشه src/) بسازید:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // لاگ کردن هر درخواست
  console.log(`[Middleware] ${request.method} ${request.nextUrl.pathname}`);

  // ادامه پردازش درخواست
  return NextResponse.next();
}

// تعیین مسیرهایی که Middleware روشون اجرا بشه
export const config = {
  matcher: [
    // همه مسیرها به جز فایل‌های استاتیک و API داخلی Next.js
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
};

یه نکته مهم: Next.js فقط یک فایل Middleware رو پشتیبانی می‌کنه. نمی‌تونید چندین فایل middleware در پوشه‌های مختلف داشته باشید. ولی نگران نباشید — بعداً یاد می‌گیریم چطور منطق‌های مختلف رو تو ماژول‌های جدا سازمان‌دهی کنیم و همه‌شون رو تو یه فایل ترکیب کنیم.

درک Matcher: کدوم مسیرها پردازش بشن؟

بخش config.matcher تعیین می‌کنه Middleware روی کدوم مسیرها اجرا بشه. اگه matcher تعریف نکنید، Middleware روی همه درخواست‌ها اجرا میشه — از جمله فایل‌های استاتیک و تصاویر که اصلاً نمی‌خواید.

چند الگوی رایج matcher:

export const config = {
  matcher: [
    // فقط مسیرهای داشبورد
    "/dashboard/:path*",

    // فقط API Routes
    "/api/:path*",

    // همه مسیرها به جز فایل‌های استاتیک
    "/((?!_next/static|_next/image|favicon.ico).*)",

    // ترکیبی: فقط مسیرهای محافظت‌شده
    "/dashboard/:path*",
    "/admin/:path*",
    "/api/protected/:path*",
  ],
};

از regex کامل پشتیبانی میشه، ولی مقادیر matcher باید ثابت باشن تا در زمان بیلد قابل تحلیل باشن. پس نمی‌تونید از متغیرها استفاده کنید.

الگوی ۱: محافظت از مسیرها با بررسی احراز هویت

رایج‌ترین کاربرد Middleware بررسی احراز هویت قبل از دسترسی به مسیرهای محافظت‌شده‌ست. اگه مقاله احراز هویت با Auth.js v5 رو خوندید، اونجا از Middleware برای بررسی نشست استفاده کردیم. حالا بیاید یه نسخه کامل‌تر و عملی‌تر ببینیم.

بررسی JWT در Edge Runtime

چون Edge Runtime از ماژول crypto نود پشتیبانی نمی‌کنه، نمی‌تونید از کتابخانه jsonwebtoken استفاده کنید. به جاش از کتابخانه jose استفاده کنید که با Web Crypto API کار می‌کنه:

npm install jose

حالا بیاید منطق بررسی توکن رو بنویسیم:

// lib/middleware/auth.ts
import { jwtVerify } from "jose";
import { NextRequest, NextResponse } from "next/server";

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || "your-secret-key"
);

// مسیرهای عمومی که نیاز به احراز هویت ندارن
const PUBLIC_PATHS = ["/", "/login", "/register", "/forgot-password"];

export async function authMiddleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // مسیرهای عمومی — بدون بررسی
  if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // دریافت توکن از کوکی
  const token = request.cookies.get("auth-token")?.value;

  if (!token) {
    // ریدایرکت به صفحه لاگین با ذخیره مسیر فعلی
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    // تأیید JWT با کتابخانه jose
    const { payload } = await jwtVerify(token, JWT_SECRET);

    // اضافه کردن اطلاعات کاربر به هدرهای درخواست
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-user-id", payload.sub as string);
    requestHeaders.set("x-user-role", payload.role as string);

    // بررسی دسترسی ادمین
    if (pathname.startsWith("/admin") && payload.role !== "admin") {
      return NextResponse.redirect(new URL("/403", request.url));
    }

    return NextResponse.next({
      request: { headers: requestHeaders },
    });
  } catch (error) {
    // توکن نامعتبر یا منقضی‌شده
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);

    const response = NextResponse.redirect(loginUrl);
    // حذف کوکی نامعتبر
    response.cookies.delete("auth-token");
    return response;
  }
}

نکته امنیتی مهم: دفاع در عمق

اینجا باید یه چیز خیلی مهم بگم: هرگز فقط به Middleware برای احراز هویت اتکا نکنید. در اوایل ۲۰۲۵، آسیب‌پذیری CVE-2025-29927 کشف شد که امکان دور زدن Middleware رو فراهم می‌کرد. حتماً Next.js رو به نسخه ۱۵.۲.۳ یا بالاتر آپدیت کنید.

رویکرد صحیح دفاع در عمق (Defense in Depth) هست — یعنی چند لایه بررسی:

  • Middleware: بررسی سریع اولیه و ریدایرکت کاربران غیرمجاز
  • Server Components: بررسی مجدد نشست قبل از رندر محتوای حساس
  • Server Actions: تأیید مجوز قبل از هر عملیات تغییر داده
  • API Routes: اعتبارسنجی توکن در هر endpoint

من خودم بعد از اون ماجرای CVE، عادت کردم همیشه حداقل دو لایه بررسی داشته باشم. Middleware به‌عنوان فیلتر اولیه عالیه، ولی لایه اصلی امنیت باید تو Server Component یا API Route باشه.

// app/dashboard/page.tsx — بررسی مجدد در Server Component
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();

  // بررسی مجدد — حتی اگه Middleware هم چک کرده
  if (!session) {
    redirect("/login");
  }

  return (
    

داشبورد

خوش اومدید، {session.user.name}

); }

الگوی ۲: محدودسازی نرخ (Rate Limiting)

محدودسازی نرخ درخواست‌ها یکی از مهم‌ترین اقدامات امنیتیه که خیلی از توسعه‌دهنده‌ها (حتی باتجربه‌ها) ازش غافل میشن. با Middleware می‌تونید قبل از رسیدن درخواست به API Routes نرخ رو محدود کنید.

پیاده‌سازی Rate Limiter ساده

برای محیط توسعه و پروژه‌های کوچک، یه rate limiter مبتنی بر حافظه (in-memory) می‌تونه کافی باشه. ولی یه هشدار: برای پروداکشن حتماً از یه store مشترک مثل Redis استفاده کنید — بعداً توضیح میدم چرا.

// lib/middleware/rate-limit.ts
import { NextRequest, NextResponse } from "next/server";

// ذخیره‌سازی درخواست‌ها در حافظه
// ⚠️ فقط برای توسعه و پروژه‌های تک‌نمونه‌ای مناسبه
const rateLimitMap = new Map<
  string,
  { count: number; lastReset: number }
>();

interface RateLimitConfig {
  windowMs: number; // بازه زمانی (میلی‌ثانیه)
  maxRequests: number; // حداکثر درخواست در بازه
}

// تنظیمات متفاوت برای مسیرهای مختلف
const RATE_LIMITS: Record = {
  "/api/auth": { windowMs: 15 * 60 * 1000, maxRequests: 5 }, // 5 درخواست در 15 دقیقه
  "/api": { windowMs: 15 * 60 * 1000, maxRequests: 100 }, // 100 درخواست در 15 دقیقه
};

function getRateLimitConfig(pathname: string): RateLimitConfig {
  // بررسی مسیرهای خاص اول
  for (const [path, config] of Object.entries(RATE_LIMITS)) {
    if (pathname.startsWith(path)) {
      return config;
    }
  }
  // پیش‌فرض
  return { windowMs: 60 * 1000, maxRequests: 60 };
}

export function rateLimitMiddleware(request: NextRequest) {
  const ip =
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
    request.ip ||
    "unknown";
  const { pathname } = request.nextUrl;
  const config = getRateLimitConfig(pathname);
  const key = `${ip}:${pathname}`;
  const now = Date.now();

  const entry = rateLimitMap.get(key);

  if (!entry || now - entry.lastReset > config.windowMs) {
    // بازه جدید
    rateLimitMap.set(key, { count: 1, lastReset: now });
  } else if (entry.count >= config.maxRequests) {
    // حد مجاز رد شده
    const retryAfter = Math.ceil(
      (config.windowMs - (now - entry.lastReset)) / 1000
    );

    return new NextResponse(
      JSON.stringify({
        error: "Too Many Requests",
        retryAfter,
      }),
      {
        status: 429,
        headers: {
          "Content-Type": "application/json",
          "Retry-After": retryAfter.toString(),
          "X-RateLimit-Limit": config.maxRequests.toString(),
          "X-RateLimit-Remaining": "0",
        },
      }
    );
  } else {
    entry.count++;
  }

  // اضافه کردن هدرهای اطلاعاتی
  const response = NextResponse.next();
  const remaining = config.maxRequests - (rateLimitMap.get(key)?.count || 0);
  response.headers.set("X-RateLimit-Limit", config.maxRequests.toString());
  response.headers.set("X-RateLimit-Remaining", remaining.toString());

  return response;
}

چرا برای پروداکشن Redis لازمه؟

Rate limiter مبتنی بر حافظه یه مشکل بزرگ داره: بین نمونه‌های مختلف سرور به اشتراک گذاشته نمیشه. اگه اپلیکیشن شما روی چند نمونه اجرا بشه (که تو Vercel و پلتفرم‌های سرورلس معمولاً همینطوره)، هر نمونه شمارنده جدای خودش رو داره. یعنی یه مهاجم می‌تونه با تقسیم درخواست‌ها بین نمونه‌ها، محدودیت رو دور بزنه.

برای پروداکشن از Upstash Redis استفاده کنید که هم Edge-compatible هست و هم تیر رایگان خوبی داره:

npm install @upstash/ratelimit @upstash/redis
// lib/middleware/rate-limit-redis.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse } from "next/server";

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// الگوریتم Sliding Window — دقیق‌تر از Fixed Window
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(100, "15 m"),
  analytics: true,
});

export async function rateLimitRedisMiddleware(request: NextRequest) {
  const ip =
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
    request.ip ||
    "anonymous";

  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return new NextResponse(
      JSON.stringify({ error: "Too Many Requests" }),
      {
        status: 429,
        headers: {
          "Content-Type": "application/json",
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
          "X-RateLimit-Reset": reset.toString(),
        },
      }
    );
  }

  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", limit.toString());
  response.headers.set("X-RateLimit-Remaining", remaining.toString());
  return response;
}

الگوی ۳: هدرهای امنیتی

هدرهای امنیتی HTTP صادقانه یکی از ساده‌ترین و درعین‌حال مؤثرترین روش‌های محافظت از اپلیکیشن هستن. با Middleware می‌تونید این هدرها رو به‌صورت متمرکز روی همه پاسخ‌ها تنظیم کنید — یه بار بنویس، همه‌جا اعمال بشه.

// lib/middleware/security-headers.ts
import { NextRequest, NextResponse } from "next/server";

export function securityHeadersMiddleware(request: NextRequest) {
  const response = NextResponse.next();
  const nonce = crypto.randomUUID();

  // جلوگیری از Clickjacking
  response.headers.set("X-Frame-Options", "DENY");

  // جلوگیری از MIME-type sniffing
  response.headers.set("X-Content-Type-Options", "nosniff");

  // کنترل اطلاعات Referrer
  response.headers.set(
    "Referrer-Policy",
    "strict-origin-when-cross-origin"
  );

  // غیرفعال کردن دسترسی به دوربین، میکروفون و موقعیت مکانی
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=()"
  );

  // Strict Transport Security — فقط HTTPS
  response.headers.set(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains; preload"
  );

  // شناسه یکتای درخواست برای ردیابی
  response.headers.set("X-Request-Id", nonce);

  return response;
}

Content Security Policy (CSP) — سپر اصلی در برابر XSS

CSP مهم‌ترین هدر امنیتیه که خیلی از توسعه‌دهنده‌ها ازش صرف‌نظر می‌کنن — چون پیکربندیش یه‌کم پیچیده‌ست. ولی ارزشش رو داره. بیاید با هم یه CSP درست و حسابی بنویسیم:

// lib/middleware/csp.ts
import { NextRequest, NextResponse } from "next/server";

export function cspMiddleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");

  const cspDirectives = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' blob: data: https:`,
    `font-src 'self' https://fonts.gstatic.com`,
    `connect-src 'self' https://api.example.com`,
    `frame-ancestors 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
  ];

  const csp = cspDirectives.join("; ");

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });

  response.headers.set("Content-Security-Policy", csp);
  return response;
}

با این تنظیمات، فقط اسکریپت‌هایی اجرا میشن که nonce صحیح داشته باشن یا از دامنه خودتون لود بشن. این واقعاً یکی از مؤثرترین راه‌های جلوگیری از حملات XSS (Cross-Site Scripting) هست.

الگوی ۴: مسیریابی چندزبانه (i18n)

اگه اپلیکیشن شما چندزبانه‌ست، Middleware بهترین جا برای تشخیص زبان کاربر و هدایتش به نسخه مناسبه. یه نکته‌ای که باید بدونید: در App Router نسخه ۱۵، پیکربندی i18n داخلی Pages Router حذف شده و دیگه باید خودتون این کار رو انجام بدید.

// lib/middleware/i18n.ts
import { NextRequest, NextResponse } from "next/server";

const SUPPORTED_LOCALES = ["fa", "en", "ar"];
const DEFAULT_LOCALE = "fa";

function getPreferredLocale(request: NextRequest): string {
  // ۱. اول کوکی رو چک کن (کاربر قبلاً انتخاب کرده)
  const cookieLocale = request.cookies.get("preferred-locale")?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
    return cookieLocale;
  }

  // ۲. بعد هدر Accept-Language رو بررسی کن
  const acceptLanguage = request.headers.get("accept-language");
  if (acceptLanguage) {
    const languages = acceptLanguage
      .split(",")
      .map((lang) => {
        const [code, priority] = lang.trim().split(";q=");
        return {
          code: code.split("-")[0].toLowerCase(),
          priority: priority ? parseFloat(priority) : 1,
        };
      })
      .sort((a, b) => b.priority - a.priority);

    for (const lang of languages) {
      if (SUPPORTED_LOCALES.includes(lang.code)) {
        return lang.code;
      }
    }
  }

  // ۳. پیش‌فرض
  return DEFAULT_LOCALE;
}

export function i18nMiddleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // آیا URL قبلاً پیشوند زبان داره؟
  const hasLocalePrefix = SUPPORTED_LOCALES.some(
    (locale) =>
      pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (hasLocalePrefix) {
    return NextResponse.next();
  }

  // تشخیص زبان و ریدایرکت
  const locale = getPreferredLocale(request);
  const newUrl = new URL(`/${locale}${pathname}`, request.url);

  return NextResponse.redirect(newUrl);
}

این پیاده‌سازی سه لایه تشخیص زبان داره: اول کوکی (انتخاب قبلی کاربر)، بعد هدر مرورگر و در نهایت زبان پیش‌فرض. تجربه من نشون داده این رویکرد سه‌لایه‌ای بهترین تجربه کاربری رو ایجاد می‌کنه — مخصوصاً برای کاربران برگشتی که همیشه زبان ترجیحی‌شون رو می‌بینن.

الگوی ۵: تست A/B بدون جاوااسکریپت کلاینت

این یکی از جذاب‌ترین کاربردهای Middleware به نظر من. اجرای تست A/B روی Edge. مزیت اصلیش نسبت به تست A/B سمت کلاینت اینه که هیچ فلاش یا لرزش محتوا (flicker) وجود نداره — چون تصمیم‌گیری قبل از رسیدن پاسخ به مرورگر انجام میشه.

// lib/middleware/ab-test.ts
import { NextRequest, NextResponse } from "next/server";

interface ABTestConfig {
  name: string;
  variants: string[];
  weights?: number[]; // توزیع درصدی (اختیاری — پیش‌فرض مساوی)
}

const AB_TESTS: Record = {
  "/pricing": {
    name: "pricing-page-redesign",
    variants: ["control", "variant-a"],
    weights: [0.5, 0.5], // ۵۰/۵۰
  },
  "/landing": {
    name: "landing-hero-test",
    variants: ["control", "variant-a", "variant-b"],
    weights: [0.34, 0.33, 0.33],
  },
};

function selectVariant(config: ABTestConfig): string {
  const weights = config.weights || config.variants.map(
    () => 1 / config.variants.length
  );
  const random = Math.random();
  let cumulative = 0;

  for (let i = 0; i < weights.length; i++) {
    cumulative += weights[i];
    if (random <= cumulative) {
      return config.variants[i];
    }
  }
  return config.variants[0];
}

export function abTestMiddleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const testConfig = AB_TESTS[pathname];

  if (!testConfig) {
    return NextResponse.next();
  }

  const cookieName = `ab-${testConfig.name}`;

  // آیا کاربر قبلاً تو یه گروه قرار گرفته؟
  const existingVariant = request.cookies.get(cookieName)?.value;

  if (existingVariant && testConfig.variants.includes(existingVariant)) {
    // rewrite به نسخه مناسب
    if (existingVariant !== "control") {
      return NextResponse.rewrite(
        new URL(`${pathname}-${existingVariant}`, request.url)
      );
    }
    return NextResponse.next();
  }

  // انتخاب تصادفی و ذخیره در کوکی
  const variant = selectVariant(testConfig);
  let response: NextResponse;

  if (variant !== "control") {
    response = NextResponse.rewrite(
      new URL(`${pathname}-${variant}`, request.url)
    );
  } else {
    response = NextResponse.next();
  }

  // ذخیره انتخاب برای ۳۰ روز — کاربر همیشه همون نسخه رو ببینه
  response.cookies.set(cookieName, variant, {
    maxAge: 30 * 24 * 60 * 60,
    path: "/",
    sameSite: "lax",
  });

  // هدر برای تحلیل‌گر
  response.headers.set("X-AB-Test", testConfig.name);
  response.headers.set("X-AB-Variant", variant);

  return response;
}

نکته مهم: از NextResponse.rewrite استفاده می‌کنیم، نه redirect. فرقش چیه؟ URL تو مرورگر کاربر تغییر نمی‌کنه ولی محتوای متفاوتی سرو میشه. کاربر اصلاً نمی‌فهمه که داره نسخه متفاوتی رو می‌بینه — و دقیقاً همینه که می‌خوایم.

ترکیب همه الگوها: Middleware سازمان‌دهی‌شده

خب حالا که ۵ الگوی مختلف رو یاد گرفتیم، سؤال اصلی اینه: چطور همه رو تو یه فایل Middleware ترکیب کنیم بدون اینکه کد شلوغ و غیرقابل نگهداری بشه؟

پیشنهاد من ساختار ماژولاره:

project-root/
├── lib/
│   └── middleware/
│       ├── auth.ts             # محافظت از مسیرها
│       ├── rate-limit.ts       # محدودسازی نرخ
│       ├── security-headers.ts # هدرهای امنیتی
│       ├── i18n.ts             # چندزبانه
│       └── ab-test.ts          # تست A/B
└── middleware.ts                # فایل اصلی — ترکیب همه

و فایل اصلی Middleware:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { authMiddleware } from "@/lib/middleware/auth";
import { rateLimitMiddleware } from "@/lib/middleware/rate-limit";
import { securityHeadersMiddleware } from "@/lib/middleware/security-headers";
import { i18nMiddleware } from "@/lib/middleware/i18n";
import { abTestMiddleware } from "@/lib/middleware/ab-test";

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // ۱. هدرهای امنیتی — روی همه درخواست‌ها
  const securityResponse = securityHeadersMiddleware(request);

  // ۲. Rate Limiting — فقط روی API Routes
  if (pathname.startsWith("/api")) {
    const rateLimitResponse = rateLimitMiddleware(request);
    if (rateLimitResponse.status === 429) {
      return rateLimitResponse;
    }
  }

  // ۳. احراز هویت — روی مسیرهای محافظت‌شده
  if (
    pathname.startsWith("/dashboard") ||
    pathname.startsWith("/admin") ||
    pathname.startsWith("/api/protected")
  ) {
    const authResponse = await authMiddleware(request);
    if (authResponse.status === 307 || authResponse.status === 308) {
      return authResponse;
    }
  }

  // ۴. تست A/B — روی صفحات خاص
  const abResponse = abTestMiddleware(request);
  if (abResponse !== NextResponse.next()) {
    // کپی هدرهای امنیتی به پاسخ A/B
    securityResponse.headers.forEach((value, key) => {
      abResponse.headers.set(key, value);
    });
    return abResponse;
  }

  // ۵. i18n — روی صفحات عمومی
  if (
    !pathname.startsWith("/api") &&
    !pathname.startsWith("/dashboard")
  ) {
    const i18nResponse = i18nMiddleware(request);
    if (i18nResponse.status === 307) {
      return i18nResponse;
    }
  }

  return securityResponse;
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
};

ترتیب اجرا مهمه و دلیل داره:

  • هدرهای امنیتی اول اجرا میشن — چون روی همه پاسخ‌ها باید اعمال بشن
  • Rate Limiting بعدش — اگه درخواست بیش از حد باشه، نیازی به بقیه بررسی‌ها نیست
  • احراز هویت سوم — اگه کاربر مجاز نباشه، بقیه الگوها بی‌معنی هستن
  • A/B Testing و i18n آخر — فقط برای درخواست‌های مجاز و معتبر اجرا میشن

خطاهای رایج و بهترین شیوه‌ها

بعد از کار کردن با Middleware تو پروژه‌های مختلف، اینا خطاهایی هستن که بیشتر دیدم (و بعضی‌هاشون رو خودم هم تجربه کردم):

۱. حلقه بی‌نهایت ریدایرکت

این رایج‌ترین خطاست و معمولاً اولین باری که با Middleware کار می‌کنید گرفتارش میشید. اگه Middleware کاربر رو به صفحه لاگین ریدایرکت کنه ولی صفحه لاگین هم تو matcher باشه — بله، حلقه بی‌نهایت!

// ❌ اشتباه: صفحه لاگین هم بررسی میشه
export function middleware(request: NextRequest) {
  const token = request.cookies.get("token");
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
    // کاربر به /login میره → Middleware دوباره اجرا میشه
    // → توکن نداره → دوباره ریدایرکت → حلقه بی‌نهایت!
  }
}

// ✅ درست: مسیرهای عمومی رو مستثنی کنید
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const PUBLIC_PATHS = ["/login", "/register", "/"];

  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  const token = request.cookies.get("token");
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

۲. فراموش کردن استثنای فایل‌های استاتیک

اگه matcher رو درست تنظیم نکنید، Middleware حتی روی فایل‌های CSS، JS و تصاویر هم اجرا میشه. نتیجه؟ عملکرد سایت به شدت افت می‌کنه و ممکنه اصلاً متوجه دلیلش نشید.

// ✅ همیشه فایل‌های استاتیک رو مستثنی کنید
export const config = {
  matcher: [
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};

۳. عملیات سنگین در Middleware

Middleware باید سبک و سریع باشه. اگه کوئری دیتابیس یا درخواست API سنگین تو Middleware اجرا کنید، تمام درخواست‌های سایت کند میشن.

  • بد: کوئری مستقیم دیتابیس در هر درخواست
  • خوب: بررسی JWT محلی (بدون درخواست شبکه‌ای)
  • بد: واکشی پروفایل کامل کاربر از API
  • خوب: ذخیره اطلاعات ضروری (role، permissions) داخل خود JWT

۴. فراموش کردن return NextResponse.next()

اگه در هیچ شرطی NextResponse.next() رو برنگردونید، درخواست بلاک میشه و کاربر با صفحه خالی مواجه میشه. همیشه مطمئن بشید که یه پاسخ پیش‌فرض دارید.

سؤالات متداول

آیا Middleware روی هر درخواست اجرا میشه؟

بله، به‌طور پیش‌فرض Middleware روی هر درخواست (از جمله فایل‌های استاتیک) اجرا میشه. به همین دلیل استفاده از config.matcher خیلی مهمه تا فقط روی مسیرهای مورد نیاز اجرا بشه و عملکرد سایت تحت تأثیر قرار نگیره.

آیا می‌تونم چند فایل Middleware داشته باشم؟

نه متأسفانه. Next.js فقط یک فایل Middleware رو پشتیبانی می‌کنه (middleware.ts در ریشه پروژه یا src/). ولی همون‌طور که تو بخش «ترکیب همه الگوها» دیدید، می‌تونید منطق‌های مختلف رو در ماژول‌های جداگانه بنویسید و تو فایل اصلی ترکیبشون کنید.

تفاوت redirect و rewrite در Middleware چیه؟

redirect آدرس URL رو تو مرورگر تغییر میده و کاربر متوجه انتقال میشه (کد وضعیت ۳۰۷/۳۰۸). ولی rewrite بدون تغییر URL، محتوای صفحه دیگه‌ای رو سرو می‌کنه — ایده‌آل برای تست A/B و پروکسی API.

چرا نمی‌تونم از کتابخانه jsonwebtoken در Middleware استفاده کنم؟

چون Middleware روی Edge Runtime اجرا میشه که مبتنی بر Web API هست، نه Node.js. کتابخانه jsonwebtoken به ماژول crypto نود وابسته‌ست که در Edge موجود نیست. راه‌حل: از کتابخانه jose استفاده کنید که با Web Crypto API سازگاره.

آیا Middleware برای SEO تأثیری داره؟

قطعاً بله! Middleware مستقیماً روی SEO تأثیر داره. ریدایرکت‌های صحیح (۳۰۱/۳۰۸ برای دائمی) به حفظ رتبه کمک می‌کنن. مسیریابی i18n باعث میشه محتوای چندزبانه درست ایندکس بشه. هدرهای امنیتی هم از نظر موتورهای جستجو امتیاز مثبت محسوب میشن.

درباره نویسنده Editorial Team

Our team of expert writers and editors.