میدلور Next.js 15: الگوهای پیشرفته از احراز هویت تا Rate Limiting در Edge Runtime

راهنمای کامل میدلور Next.js 15 در Edge Runtime: از احراز هویت سبک با jose، Rate Limiting با Upstash Redis، تا هدرهای امنیتی CSP و HSTS، و مهاجرت به proxy.ts در Next.js 16.

به‌روزرسانی: ۲۸ مه ۲۰۲۶

میدلور Next.js 15 یک لایه سرور-ساید است که قبل از رسیدن درخواست به Route Handler یا صفحه‌ی موردنظر اجرا می‌شود و در Edge Runtime به‌صورت سراسری اجرا می‌گردد؛ بنابراین ابزار درست برای احراز هویت، بازنویسی URL، تزریق هدرهای امنیتی و Rate Limiting در نزدیک‌ترین Edge به کاربر است. در این راهنما الگوهایی را پوشش می‌دهم که در پروژه‌های واقعی به آن‌ها برخورده‌ام؛ از middleware.ts کلاسیک تا تغییر نام آن به proxy.ts در Next.js 16، محدودیت‌های Edge Runtime، و راه‌حل‌های عملی برای Rate Limiting با Upstash Redis.

  • میدلور در Edge Runtime اجرا می‌شود؛ یعنی به APIهای Node مانند fs و child_process و درایورهای TCP پایگاه‌داده دسترسی ندارید.
  • برای کاهش هزینه و تأخیر، حتماً از matcher استفاده کنید تا میدلور روی فایل‌های استاتیک و asset ها اجرا نشود.
  • Rate Limiting در Edge با @upstash/ratelimit و Redis از طریق REST کار می‌کند؛ کلاینت‌های TCP کلاسیک Redis در Edge اجرا نمی‌شوند.
  • برای احراز هویت سنگین (مثل تأیید JWT با کلیدهای RSA یا دسترسی به دیتابیس)، فقط وجود کوکی را در میدلور چک کنید و اعتبارسنجی واقعی را در Route Handler یا Server Component انجام دهید.
  • از Next.js 16 به بعد فایل middleware.ts به proxy.ts تغییر نام داده است؛ منطق و API یکسان است.
  • هدرهای امنیتی مثل CSP، HSTS، X-Frame-Options و Referrer-Policy را در میدلور تنظیم کنید تا روی همه پاسخ‌ها اعمال شوند.

میدلور Next.js چیست و چگونه کار می‌کند؟

میدلور تابعی است که در یک فایل به نام middleware.ts در ریشه پروژه (در کنار پوشه app یا pages) قرار می‌گیرد و قبل از پاسخگویی Next.js به درخواست اجرا می‌شود. این کد روی Edge Runtime اجرا می‌شود؛ یعنی روی هزاران نقطه نزدیک به کاربر در سراسر دنیا توزیع شده است، با Cold Start تقریباً صفر و سربار اجرای زیر-میلی‌ثانیه. از روزهای Pages Router با میدلور کار می‌کنم و در App Router نقش آن مهم‌تر هم شده، چون می‌توانید چندین نوع منطق سراسری (auth، rewriteها، headerها، logging) را در یک نقطه متمرکز کنید بدون اینکه نیاز به HOC یا wrapper component داشته باشید.

چرخه حیات درخواست در میدلور این‌طور است: ابتدا CDN/Cache چک می‌شود، سپس میدلور اجرا می‌گردد (می‌تواند درخواست را redirect یا rewrite کند، هدر اضافه کند، یا کلاً NextResponse.next() برگرداند)، و در نهایت Route Handler یا React Server Component اجرا می‌شود. یک نکته مهم: میدلور قبل از کشینگ اجرا می‌شود، یعنی حتی اگر صفحه‌ای از کش سرو شود، میدلور باز هم اجرا خواهد شد. این هم نقطه قوت است (می‌توانید A/B testing بکنید بدون اینکه به کش آسیب بزنید) و هم نقطه ضعف (نباید کاری کنید که هر درخواست را کند کند).

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

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

// مهم: matcher را تنظیم کنید تا روی asset ها اجرا نشود
export const config = {
  matcher: [
    // هر مسیر به‌جز فایل‌های Next داخلی، static، image و favicon
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

طبق مستندات رسمی میدلور Next.js، تنها یک فایل میدلور در هر پروژه پشتیبانی می‌شود؛ اما داخل آن می‌توانید بر اساس pathname شاخه‌بندی کنید یا توابع کوچک‌تر مثل handleAuth و handleRateLimit را به‌ترتیب صدا بزنید.

محدودیت‌های Edge Runtime که باید بدانید

Edge Runtime یک محیط اجرای جاوااسکریپت سبک است که روی موتور V8 (همان موتور Chrome و Node.js) ساخته شده، اما به جای Container کامل، در sandboxهای کوچکی به نام V8 Isolate اجرا می‌شود. این مدل معماری دلیل اصلی سرعت Cold Start آن است، اما هزینه‌ای هم دارد: شما به APIهای کامل Node دسترسی ندارید. فقط Web Standard APIها در دسترس‌اند: fetch، Request/Response، URL، Headers، crypto.subtle، Streams و چیزهای مشابه.

این یعنی نمی‌توانید در میدلور:

  • از fs برای خواندن فایل استفاده کنید
  • درایور TCP پایگاه‌داده (مثل pg، mysql2، MongoDB driver) را import کنید
  • از ORMهایی مثل Prisma client کلاسیک یا Drizzle با درایور Node استفاده کنید
  • کتابخانه‌های سنگین Node مثل jsonwebtoken با وابستگی به crypto Node را به کار ببرید

راه‌حل‌های جایگزین: برای پایگاه‌داده، از کلاینت‌های Edge-compatible مثل Neon Serverless، Upstash Redis (HTTP REST)، یا PlanetScale Edge استفاده کنید. برای JWT، کتابخانه jose را به‌جای jsonwebtoken به کار ببرید چون از Web Crypto استفاده می‌کند. اگر می‌خواهید عمیق‌تر در این محدودیت‌ها بروید، مرجع رسمی Edge Runtime فهرست کامل APIهای پشتیبانی‌شده را دارد.

الگوی احراز هویت در میدلور: چک سبک، تأیید سنگین

یکی از معمول‌ترین کاربردهای میدلور، گارد کردن مسیرهایی مثل /dashboard یا /api/admin است. اما اینجا یک تله رایج وجود دارد: وسوسه می‌شوید کل اعتبارسنجی JWT را در میدلور انجام دهید، با کلید عمومی، چک expiry، چک audience و حتی query زدن به DB برای بررسی revocation. این کار میدلور را سنگین می‌کند و چون روی هر درخواست محافظت‌شده اجرا می‌شود، تأخیر کل اپ بالا می‌رود. اگر روی الگوهای کامل احراز هویت در App Router کار می‌کنید، راهنمای احراز هویت در Next.js App Router با Auth.js v5 این تفکیک مسئولیت را با مثال‌های کامل پوشش می‌دهد.

الگوی پیشنهادی من «چک سبک در میدلور، تأیید سنگین در Route Handler» است:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const PUBLIC_PATHS = ['/login', '/register', '/api/auth'];

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

  // اجازه عبور به مسیرهای عمومی
  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  const token = request.cookies.get('session')?.value;

  // چک سبک: فقط وجود کوکی و امضای آن
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
    const { payload } = await jwtVerify(token, secret);

    // پاس کردن user id به downstream از طریق هدر
    const response = NextResponse.next();
    response.headers.set('x-user-id', String(payload.sub));
    return response;
  } catch {
    // کوکی نامعتبر — پاک کن و بفرست به login
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('session');
    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/((?!auth).*)'],
};

توجه کنید که از jose استفاده می‌کنم چون Edge-compatible است. چک‌های پیچیده‌تر مانند نقش (role)، دسترسی به منبع خاص، یا revocation list را در Route Handler یا Server Action انجام دهید؛ آنجا به دیتابیس کامل دسترسی دارید و این منطق فقط زمانی اجرا می‌شود که واقعاً به آن نیاز است، نه روی هر درخواست.

پیاده‌سازی Rate Limiting در Edge با Upstash

Rate Limiting در Edge یکی از قوی‌ترین کاربردهای میدلور است: قبل از اینکه درخواست به Serverless Function برسد، آن را reject می‌کنید، یعنی هم در هزینه compute صرفه‌جویی می‌کنید و هم در نزدیک‌ترین نقطه به مهاجم آن را بلاک می‌کنید. مشکل: کلاینت‌های Redis کلاسیک از TCP استفاده می‌کنند که در Edge Runtime در دسترس نیست. راه‌حل استاندارد امروز پکیج رسمی Upstash Ratelimit است که از REST API استفاده می‌کند.

نصب:

pnpm add @upstash/ratelimit @upstash/redis

پیاده‌سازی کامل با Sliding Window و کش Ephemeral:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// مهم: خارج از تابع middleware تعریف کنید تا بین درخواست‌های Hot یکسان بماند
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  // ۱۰ درخواست در هر ۱۰ ثانیه با الگوریتم Sliding Window
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
  prefix: 'rl:api',
  // کش حافظه‌ای داخل isolate برای کاهش رفت‌وآمد به Redis
  ephemeralCache: new Map(),
});

export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  // شناسایی کاربر: ابتدا session، در غیر این صورت IP
  const userId = request.cookies.get('session')?.value;
  const ip =
    request.headers.get('x-forwarded-for')?.split(',')[0].trim() ??
    request.headers.get('x-real-ip') ??
    '127.0.0.1';
  const identifier = userId ?? ip;

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

  const headers = new Headers({
    'X-RateLimit-Limit': String(limit),
    'X-RateLimit-Remaining': String(remaining),
    'X-RateLimit-Reset': String(reset),
  });

  if (!success) {
    return new NextResponse(
      JSON.stringify({ error: 'تعداد درخواست‌ها از حد مجاز عبور کرده است' }),
      { status: 429, headers: { ...Object.fromEntries(headers), 'Content-Type': 'application/json' } }
    );
  }

  const response = NextResponse.next();
  headers.forEach((v, k) => response.headers.set(k, v));
  return response;
}

export const config = {
  matcher: ['/api/:path*'],
};

چرا IP از request.ip را استفاده نکردم؟

در Next.js 15+ پراپرتی request.ip روی همه پلتفرم‌ها قابل اطمینان نیست. روی Vercel کار می‌کند، اما اگر پشت Cloudflare یا یک reverse proxy دیگر باشید، باید هدر x-forwarded-for را parse کنید. همیشه اولین مقدار آن لیست (که توسط CDN ست شده) IP واقعی کاربر است.

هدرهای امنیتی: CSP، HSTS و بقیه

میدلور مکان ایده‌آلی برای تزریق هدرهای امنیتی است چون روی همه پاسخ‌ها (صفحات، API routes، فایل‌های استاتیک پویا) اعمال می‌شود. پروژه OWASP Secure Headers فهرست کاملی از هدرهای پیشنهادی را دارد. در پروژه‌های production من، حداقل این هدرها را تنظیم می‌کنم:

// در middleware.ts، قبل از return response:
const securityHeaders = {
  'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
  'X-Frame-Options': 'DENY',
  'X-Content-Type-Options': 'nosniff',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  'Content-Security-Policy': [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'",
  ].join('; '),
};

Object.entries(securityHeaders).forEach(([key, value]) => {
  response.headers.set(key, value);
});

CSP سخت‌ترین هدر برای راه‌اندازی است چون اگر اشتباه تنظیم شود، اپ شما کار نمی‌کند. توصیه می‌کنم با Content-Security-Policy-Report-Only شروع کنید، چند روز گزارش‌ها را تماشا کنید، سپس به حالت enforcement سوئیچ کنید. برای صفحات Next.js که از inline script استفاده می‌کنند (به‌خاطر hydration data)، می‌توانید از nonce-based CSP استفاده کنید که از Next.js 13.4 به بعد به‌صورت رسمی پشتیبانی می‌شود.

Rewrite در برابر Redirect: کدام را کِی استفاده کنیم؟

این دو الگو در میدلور رفتار متفاوتی دارند و انتخاب اشتباه می‌تواند به SEO شما آسیب بزند یا تجربه کاربری را خراب کند.

ویژگیRedirectRewrite
تغییر URL در مرورگربله (URL جدید نمایش داده می‌شود)خیر (URL اصلی باقی می‌ماند)
درخواست HTTP اضافیبله (مرورگر درخواست دوم می‌فرستد)خیر (داخلی)
کد وضعیت HTTP3xx (307 یا 308)200 (مثل صفحه عادی)
اثر بر SEO308 برای انتقال دائمی توصیه می‌شودمحتوای یکسان روی چند URL — مراقب duplicate content باشید
کاربرد معمولLogin flow، redirect قدیمی به جدیدA/B testing، vanity URL، multi-tenant
سرعت ادراکیکندتر (دو درخواست)سریع‌تر (یک درخواست)
// مثال Rewrite: مسیریابی tenant بر اساس subdomain
export function middleware(request: NextRequest) {
  const host = request.headers.get('host') ?? '';
  const subdomain = host.split('.')[0];

  if (subdomain && subdomain !== 'www' && subdomain !== 'app') {
    const url = request.nextUrl.clone();
    url.pathname = `/tenant/${subdomain}${url.pathname}`;
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

این الگو در پروژه‌های Multi-Tenant SaaS بسیار رایج است: کاربر acme.example.com را می‌بیند، اما در Next.js مسیر /tenant/acme/dashboard رندر می‌شود. اگر می‌خواهید این الگو را با کشینگ ترکیب کنید، راهنمای واکشی داده و کشینگ در Next.js 15 توضیح می‌دهد چطور tag-based revalidation برای هر tenant جداگانه کار می‌کند.

رفع خطاهای رایج Edge Runtime

وقتی برای اولین بار از Pages Router به App Router مهاجرت کردم، تقریباً همه این خطاها را گرفتم. اینجا راه‌حل سریع هرکدام را می‌گذارم:

۱. خطای «The Edge Runtime does not support Node.js 'crypto' module»

یک کتابخانه که از require('crypto') Node استفاده می‌کند را import کرده‌اید. اگر برای hash یا HMAC به آن نیاز دارید، از Web Crypto API استفاده کنید:

// به جای crypto.createHash('sha256') از Node
async function sha256(message: string) {
  const data = new TextEncoder().encode(message);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

۲. خطای «Module not found: Can't resolve 'fs'»

این معمولاً وقتی اتفاق می‌افتد که یک کتابخانه (مثل dotenv یا یک ORM) را به‌طور غیرمستقیم وارد کرده‌اید. متغیرهای محیطی را مستقیم از process.env بخوانید (Next.js در زمان build آن‌ها را inline می‌کند). برای دیتابیس، از کلاینت‌های Edge-compatible استفاده کنید.

۳. خطای «Middleware exceeded the 1MB size limit» در Vercel

bundle میدلور شما خیلی بزرگ است. این بزرگ‌ترین تله من بود وقتی کل lodash را import کردم. راه‌حل: فقط توابعی که نیاز دارید را import کنید (import debounce from 'lodash/debounce')، یا بهتر، آن‌ها را با utility ساده جایگزین کنید. ابزار @next/bundle-analyzer به شما می‌گوید چه چیزی این حجم را اشغال کرده.

۴. Rate Limiter همیشه fail می‌کند روی هر درخواست

اگر متغیر Ratelimit را داخل تابع middleware بسازید، در هر invocation از نو ساخته می‌شود و کش ephemeral کار نمی‌کند. حتماً آن را در اسکوپ ماژول، خارج از تابع تعریف کنید.

مهاجرت از middleware.ts به proxy.ts در Next.js 16

یکی از تغییرات نام‌گذاری در Next.js 16 این بود که middleware.ts به proxy.ts تغییر نام داد و تابع export شده باید proxy نام داشته باشد. منطق و API دقیقاً یکسان است؛ فقط نام فایل و نام تابع عوض شده تا منعکس‌کننده ماهیت واقعی این لایه باشد (یک proxy قبل از rendering).

// proxy.ts (Next.js 16+)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  // همان منطق قبلی شما
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

اگر روی الگوهای مدرن‌تر App Router مثل Streaming و Suspense کار می‌کنید، راهنمای Partial Prerendering در Next.js 15 نشان می‌دهد چطور می‌توانید میدلور را با PPR ترکیب کنید تا قسمت‌های استاتیک صفحه از CDN سرو شوند در حالی که قسمت‌های شخصی‌سازی شده در Edge اجرا می‌گردند.

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

آیا میدلور Next.js می‌تواند به پایگاه داده دسترسی پیدا کند؟

به‌طور مستقیم با درایورهای کلاسیک TCP (مثل pg یا Prisma) خیر، چون Edge Runtime از TCP پشتیبانی نمی‌کند. اما با کلاینت‌های HTTP-based مثل Upstash Redis، Neon Serverless، یا PlanetScale Edge می‌توانید. بهترین الگو این است که میدلور را سبک نگه دارید و عملیات سنگین DB را در Route Handler انجام دهید.

تفاوت بین میدلور و Route Handler با runtime = 'edge' چیست؟

هر دو روی Edge اجرا می‌شوند، اما میدلور قبل از تصمیم routing اجرا می‌شود و می‌تواند درخواست را redirect/rewrite کند، در حالی که Edge Route Handler یک endpoint مشخص است که پاسخ نهایی را تولید می‌کند. میدلور برای منطق سراسری (auth، headers، rate limit) است؛ Route Handler برای منطق business یک endpoint خاص.

چرا میدلور من روی فایل‌های استاتیک اجرا می‌شود؟

چون matcher تنظیم نکرده‌اید. به‌صورت پیش‌فرض، میدلور روی هر مسیر اجرا می‌شود از جمله _next/static و تصاویر. این هم latency اضافه می‌کند و هم تعداد invocation در Vercel را چند برابر می‌کند. حتماً یک matcher با pattern منفی برای فایل‌های استاتیک تعریف کنید.

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

خیر، Next.js فقط یک فایل میدلور در ریشه پروژه را به رسمیت می‌شناسد. اما داخل آن می‌توانید بر اساس pathname یا الگوی URL منطق را تقسیم کنید و توابع جداگانه (مثل handleAuth، handleRateLimit، addSecurityHeaders) را به‌ترتیب فراخوانی کنید و کامپوزیشن آن‌ها را خودتان مدیریت کنید.

آیا Rate Limiting در میدلور برای DDoS کافی است؟

نه. میدلور Rate Limiting در لایه application خوب کار می‌کند، اما برای DDoS واقعی به دفاع لایه شبکه (مثل Cloudflare، Vercel DDoS Mitigation، یا AWS Shield) نیاز دارید. میدلور برای محدودسازی نرخ معقول (مثل جلوگیری از brute force روی login، یا محدود کردن abuse از API key) عالی است، نه برای حجم میلیون‌درخواست-بر-ثانیه.

Ben Howard
درباره نویسنده Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.