لو كنت تتابع أخبار Next.js في الفترة الأخيرة، فأنت على الأرجح لاحظت التغيير الكبير في الإصدار 16: ملف middleware.ts صار مُهملًا (deprecated) وحلّ محله proxy.ts. صراحةً، أول ما قرأت عن هذا التغيير ظننت إنه مجرد تغيير اسم — لكن الموضوع أعمق من كذا بكثير.
في هذا الدليل، راح نستعرض كل شيء عن هذا التحول: ليه تم التغيير، كيف ترحّل مشروعك، وأمثلة عملية تشمل المصادقة وتحديد المعدل والتوجيه الجغرافي واختبارات A/B. يلّا نبدأ.
ما هو Middleware في Next.js وما الذي تغيّر في الإصدار 16؟
Middleware في Next.js هو طبقة معالجة تعمل قبل وصول الطلب إلى مسارات التطبيق. يعني ببساطة، يقدر يعترض أي طلب وارد وينفّذ منطق مخصص — مثل التحقق من المصادقة، إعادة التوجيه، إعادة الكتابة (Rewrite)، تعديل الترويسات (Headers)، أو حتى يرد مباشرة. كل هذا يصير قبل ما يبدأ تصيير أي مكوّن.
ليش غيّروا الاسم من middleware إلى proxy؟
فريق Next.js ما اتخذ هذا القرار عشوائيًا. فيه أسباب وجيهة:
- تجنب الخلط مع Express.js: مصطلح "middleware" مُستخدم بكثرة في Express.js لكن بمفهوم مختلف تمامًا. كثير من المطورين كانوا يتعاملون مع middleware في Next.js وكأنه وسيط متسلسل مثل Express — وهذا غلط.
- توضيح الدور الحقيقي: مصطلح "proxy" يوضّح إن هذه الطبقة تقف عند حدود الشبكة أمام التطبيق، تمامًا مثل البروكسي العكسي (Reverse Proxy). هي مو جزء من منطق التطبيق الداخلي.
- تقليل الاستخدام المفرط: كان Middleware قوي لدرجة إن المطورين استخدموه لكل شيء (وأنا كنت منهم صراحةً). فريق Next.js يبي
proxy.tsيكون الملاذ الأخير — ويوفر بدائل أسهل وأنسب.
التغيير الأهم: بيئة التشغيل
وهنا الفرق الجوهري اللي يهمك فعلًا. التغيير مو مجرد إعادة تسمية:
middleware.tsالقديم كان يشتغل على Edge Runtime افتراضيًا — بيئة خفيفة لكنها محدودة وما تدعم كل واجهات Node.js.proxy.tsالجديد يشتغل على Node.js Runtime افتراضيًا — يعني وصول كامل لواجهات Node.js وأداء أفضل للعمليات المكثفة.
وهذا شيء كبير. تقدر الحين تستخدم أي مكتبة Node.js في البروكسي بدون قيود Edge Runtime. لكن لو كنت محتاج Edge Runtime تحديدًا (مثلًا عشان تقلل زمن الاستجابة عبر التوزيع الجغرافي)، تقدر تستمر تستخدم middleware.ts مؤقتًا — بس خلّ في بالك إنه مُهمل وبيتشال في إصدار مستقبلي.
كيفية الترحيل من middleware.ts إلى proxy.ts
الخبر الحلو: الترحيل بسيط في أغلب الحالات. عندك طريقتين.
الطريقة التلقائية: استخدام Codemod
Next.js يوفر أداة codemod تلقائية تسوي كل شيء نيابةً عنك:
npx @next/codemod@latest middleware-to-proxy .
هذا الأمر بيسوي ثلاث أشياء:
- يعيد تسمية
middleware.tsإلىproxy.ts - يغيّر اسم الدالة المُصدَّرة من
middlewareإلىproxy - يحدّث إعدادات التكوين المرتبطة
الطريقة اليدوية
لو تفضّل تسوي الترحيل بنفسك، الموضوع سهل. خلني أوريك الفرق:
قبل (Next.js 15 وما قبله):
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// منطق الوسيط
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
بعد (Next.js 16):
// proxy.ts
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*'],
}
شفت؟ التغيير الوحيد هو اسم الملف واسم الدالة. واجهة البرمجة (NextRequest، NextResponse، config.matcher) تبقى كما هي بالضبط.
تنبيه: إعدادات التكوين تغيّرت أيضًا
شيء سهل تنساه — بعض إعدادات next.config.js تغيّرت أسماؤها:
// next.config.js — قبل
module.exports = {
skipMiddlewareUrlNormalize: true,
}
// next.config.js — بعد
module.exports = {
skipProxyUrlNormalize: true,
}
موقع ملف proxy.ts وكيفية عمل المُطابق (Matcher)
ملف proxy.ts لازم يكون في جذر المشروع — أو داخل مجلد src/ لو كنت تستخدمه. المهم يكون على نفس مستوى مجلدات app أو pages.
ونقطة مهمة: ملف proxy.ts واحد فقط لكل مشروع. لكن لا تقلق — تقدر تنظّم منطقك في ملفات منفصلة وتستوردها في proxy.ts الرئيسي.
كيف يشتغل المُطابق (Matcher)؟
افتراضيًا، البروكسي يشتغل على كل طلب في مشروعك. وهذا بالطبع مو مثالي من ناحية الأداء، عشان كذا يُنصح باستخدام matcher لتحديد المسارات المستهدفة:
// proxy.ts
export const config = {
matcher: [
// مطابقة مسارات لوحة التحكم
'/dashboard/:path*',
// مطابقة مسارات API (باستثناء المسارات الثابتة)
'/api/:path*',
// استخدام تعبير نمطي لاستبعاد الملفات الثابتة
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
المُطابق يدعم المسارات البسيطة والمعلمات الديناميكية والتعبيرات النمطية. ونصيحة مهمة: لا تنسَ تستبعد الملفات الثابتة مثل الصور وملفات CSS/JS — ما في فايدة من تشغيل البروكسي على طلبات ما تحتاج معالجة.
النمط الأول: حماية المسارات بالمصادقة (Authentication)
هذا أكثر استخدام شائع للبروكسي — وغالبًا هو أول شيء بتحتاجه. إليك مثال عملي كامل:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// المسارات العامة التي لا تتطلب مصادقة
const publicPaths = ['/login', '/register', '/forgot-password', '/']
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
const token = request.cookies.get('auth-token')?.value
// السماح بالوصول للمسارات العامة
if (publicPaths.some(path => pathname === path || pathname.startsWith(path + '/'))) {
// إذا كان المستخدم مُسجلًا ويحاول الوصول لصفحة تسجيل الدخول
if (token && (pathname === '/login' || pathname === '/register')) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
// حماية المسارات الخاصة
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
تحقق متقدم باستخدام JWT في بيئة Node.js
وهنا تظهر القوة الحقيقية لـ proxy.ts. بما إنه يشتغل على Node.js Runtime في Next.js 16، تقدر تستخدم مكتبات JWT كاملة بدون قيود Edge — وهذا كان حلم كثير من المطورين:
// lib/auth.ts
import jwt from 'jsonwebtoken'
interface TokenPayload {
userId: string
role: string
exp: number
}
export function verifyToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload
} catch {
return null
}
}
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from './lib/auth'
export function proxy(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const payload = verifyToken(token)
if (!payload) {
// الرمز غير صالح أو منتهي الصلاحية
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('auth-token')
return response
}
// تمرير معلومات المستخدم عبر الترويسات
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', payload.userId)
requestHeaders.set('x-user-role', payload.role)
return NextResponse.next({
request: { headers: requestHeaders },
})
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}
ملاحظات لمستخدمي مكتبات المصادقة
لو كنت تستخدم مكتبات مثل Auth.js أو Supabase Auth أو Clerk، انتبه لهذي النقاط:
proxy.tsيشتغل على Node.js Runtime وليس Edge، فقد تحتاج تحدّث إعدادات المكتبة.- تأكد إن مكتبة المصادقة تدعم بيئة Node.js في سياق البروكسي.
- احذر من مشكلة "حلقة تسجيل الخروج" — حيث ما تُمسح ملفات تعريف الارتباط بشكل صحيح إذا ما مرّرت ترويسات الاستجابة. هذي مشكلة واجهتني شخصيًا وأخذت مني وقت أطوّلها.
النمط الثاني: إعادة التوجيه وإعادة الكتابة (Redirects & Rewrites)
إعادة التوجيه وإعادة الكتابة من أساسيات البروكسي. خلني أوضح الفرق لأنه مهم:
- إعادة التوجيه (Redirect): تغيّر عنوان URL في المتصفح (استجابة HTTP 3xx). المتصفح ينشئ طلب جديد كليًا للعنوان الجديد.
- إعادة الكتابة (Rewrite): تُبقي عنوان URL كما هو في المتصفح، لكن تقدّم محتوى من مسار مختلف داخليًا. ممتازة لاختبارات A/B وعناوين URL المُختصرة.
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// إعادة توجيه دائمة (301) — للروابط القديمة
if (pathname === '/old-blog') {
return NextResponse.redirect(new URL('/blog', request.url), 301)
}
// إعادة توجيه مؤقتة (307) — لصيانة مؤقتة
if (pathname.startsWith('/maintenance-page')) {
return NextResponse.redirect(new URL('/under-construction', request.url), 307)
}
// إعادة كتابة — URL يبقى كما هو لكن المحتوى من مسار آخر
if (pathname === '/docs') {
return NextResponse.rewrite(new URL('/documentation/latest', request.url))
}
return NextResponse.next()
}
النمط الثالث: التوجيه الجغرافي والتعريب (i18n)
البروكسي مثالي لاكتشاف موقع المستخدم الجغرافي ولغته وتوجيهه تلقائيًا. وبصراحة، هذا من أحب الأنماط عندي لأنه يحسّن تجربة المستخدم بشكل ملحوظ:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const supportedLocales = ['ar', 'en', 'fr']
const defaultLocale = 'en'
function getPreferredLocale(request: NextRequest): string {
// التحقق من ملف تعريف الارتباط أولًا
const cookieLocale = request.cookies.get('preferred-locale')?.value
if (cookieLocale && supportedLocales.includes(cookieLocale)) {
return cookieLocale
}
// استخدام ترويسة Accept-Language
const acceptLanguage = request.headers.get('accept-language') || ''
for (const locale of supportedLocales) {
if (acceptLanguage.includes(locale)) {
return locale
}
}
return defaultLocale
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// تجاهل المسارات التي تحتوي بالفعل على بادئة لغة
const hasLocalePrefix = supportedLocales.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.rewrite(newUrl)
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
}
الجميل في استخدام إعادة الكتابة هنا (بدل إعادة التوجيه) هو إن عنوان URL يبقى نظيف في المتصفح — يعني المستخدم يشوف /about بدل /ar/about، لكن المحتوى المُقدَّم يكون بلغته المفضلة.
النمط الرابع: تحديد المعدل (Rate Limiting)
حماية واجهات API من الاستغلال المفرط أمر ضروري في أي تطبيق إنتاجي. ما في نقاش. إليك تطبيق بسيط باستخدام الذاكرة المحلية كنقطة بداية:
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; lastReset: number }>()
export function rateLimit(ip: string, limit: number = 10, windowMs: number = 60000): boolean {
const now = Date.now()
const record = rateLimitMap.get(ip)
if (!record || now - record.lastReset > windowMs) {
rateLimitMap.set(ip, { count: 1, lastReset: now })
return true // مسموح
}
if (record.count >= limit) {
return false // محظور
}
record.count++
return true // مسموح
}
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from './lib/rate-limit'
export function proxy(request: NextRequest) {
// تطبيق تحديد المعدل على مسارات API فقط
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for')
?? request.headers.get('x-real-ip')
?? 'unknown'
if (!rateLimit(ip, 20, 60000)) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{
status: 429,
headers: {
'Retry-After': '60',
'X-RateLimit-Limit': '20',
'X-RateLimit-Remaining': '0',
},
}
)
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*'],
}
تنبيه مهم: الحل أعلاه يستخدم الذاكرة المحلية، يعني البيانات تضيع مع كل إعادة تشغيل للخادم ولا تتشارك بين عدة instances. للإنتاج، تحتاج حل أقوى.
تحديد معدل إنتاجي باستخدام Upstash Redis
للتطبيقات الإنتاجية الجادة، استخدم حل موزّع مثل Upstash Redis مع مكتبة @upstash/ratelimit:
// lib/rate-limit-redis.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const rateLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
})
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimiter } from './lib/rate-limit-redis'
export async function proxy(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, limit, remaining, reset } = await rateLimiter.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
)
}
return NextResponse.next()
}
النمط الخامس: اختبارات A/B بدون JavaScript في العميل
هذا النمط من أذكى استخدامات البروكسي في رأيي. تقدر تنفّذ اختبارات A/B بالكامل على الخادم بدون إضافة سطر JavaScript واحد للمتصفح. النتيجة؟ صفر وميض (flickering) وصفر تأثير على الأداء:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname === '/pricing') {
// التحقق من وجود متغير سابق في ملف تعريف الارتباط
let variant = request.cookies.get('ab-pricing')?.value
if (!variant) {
// تعيين متغير عشوائي
variant = Math.random() < 0.5 ? 'control' : 'experiment'
}
// إعادة كتابة إلى صفحة المتغير المناسب
const response = NextResponse.rewrite(
new URL('/pricing/' + variant, request.url)
)
// حفظ المتغير في ملف تعريف الارتباط لضمان الاتساق
response.cookies.set('ab-pricing', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 يومًا
httpOnly: true,
sameSite: 'lax',
})
return response
}
return NextResponse.next()
}
الفكرة بسيطة: كل مستخدم يحصل على متغير ثابت (بفضل ملف تعريف الارتباط)، وبما إن إعادة الكتابة تصير على الخادم، المستخدم ما يحس بأي شيء. تجربة سلسة تمامًا.
النمط السادس: إضافة ترويسات أمان
البروكسي مكان ممتاز لإضافة ترويسات أمان لجميع الاستجابات. وأنصحك ما تهمل هذي الخطوة حتى لو كان مشروعك صغير:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
// ترويسات أمان أساسية
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
)
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
)
return response
}
دمج أنماط متعددة في proxy.ts واحد
في الواقع العملي، غالبًا بتحتاج تدمج عدة أنماط مع بعض. الحيلة هي ترتيب المنطق بشكل صحيح — الأمان أولًا، ثم تحديد المعدل، ثم المصادقة. إليك مثال:
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from './lib/rate-limit'
import { verifyToken } from './lib/auth'
const publicPaths = ['/', '/login', '/register', '/blog']
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
const response = NextResponse.next()
// 1. ترويسات الأمان (لجميع الطلبات)
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
// 2. تحديد المعدل لمسارات API
if (pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 30, 60000)) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
}
// 3. المصادقة للمسارات المحمية
const isPublic = publicPaths.some(
p => pathname === p || pathname.startsWith(p + '/')
)
if (!isPublic && !pathname.startsWith('/api/public')) {
const token = request.cookies.get('auth-token')?.value
if (!token || !verifyToken(token)) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
أفضل الممارسات والأخطاء الشائعة
افعل
- استخدم matcher دائمًا: حدد المسارات المستهدفة بدقة. تشغيل البروكسي على كل طلب (بما فيها الصور والملفات الثابتة) مضيعة للموارد.
- اجعل البروكسي خفيف: هدفه الأساسي هو إعادة التوجيه وإعادة الكتابة وتعديل الترويسات — مو تنفيذ منطق تطبيقي ثقيل.
- استخدم الترويسات لتمرير البيانات: مرّر معلومات من البروكسي إلى التطبيق عبر الترويسات أو ملفات تعريف الارتباط.
- تعامل مع الأخطاء بعناية: أي خطأ غير معالج في البروكسي بيمنع الطلب من الوصول للتطبيق كليًا.
لا تفعل
- لا تنفّذ استعلامات قاعدة بيانات في البروكسي: حتى مع دعم Node.js Runtime، البروكسي مو المكان المناسب لعمليات قاعدة البيانات. استخدم Server Components أو Server Actions بدلًا من ذلك.
- لا تنسَ استبعاد الملفات الثابتة: تشغيل البروكسي على
_next/staticو_next/imageيبطّئ التطبيق بدون أي فائدة. - لا تُنشئ حلقات إعادة توجيه: تحقق دائمًا إنك مو بالفعل في صفحة الوجهة قبل ما تعيد التوجيه. هذا خطأ كلاسيكي يصعب اكتشافه.
- لا تعتمد على Edge Runtime مع proxy.ts: لو كنت محتاج Edge Runtime، استخدم
middleware.tsمؤقتًا وخطط للانتقال.
الأسئلة الشائعة
هل يجب أن أنتقل فورًا من middleware.ts إلى proxy.ts؟
مو بالضرورة فورًا، لكن يُنصح بذلك بشدة. middleware.ts لا يزال يشتغل في Next.js 16 لكنه مُهمل وبيتشال في إصدار مستقبلي. لو تطبيقك يعتمد على Edge Runtime، ممكن تنتظر. أما لو تستخدم Node.js Runtime، فالترحيل بسيط جدًا — مجرد إعادة تسمية.
ما الفرق بين استخدام proxy.ts وإعادة التوجيه في next.config.js؟
إعادة التوجيه في next.config.js مناسبة للتوجيهات الثابتة البسيطة. أما proxy.ts فيتيح لك منطق برمجي كامل — تتحقق من ملفات تعريف الارتباط، تقرأ الترويسات، تنفّذ شروط معقدة. القاعدة بسيطة: لو التوجيه ثابت ومعروف مسبقًا، استخدم next.config.js. لو تحتاج منطق ديناميكي، استخدم proxy.ts.
هل يمكنني استخدام proxy.ts مع استضافة ذاتية بدون Vercel؟
نعم، proxy.ts يشتغل مع أي بيئة استضافة تدعم Next.js. لكن بعض الميزات مثل request.geo متاحة فقط على Vercel Edge. في الاستضافة الذاتية، بتحتاج حلول بديلة لاكتشاف الموقع الجغرافي. ونقطة مهمة: OpenNext لا يدعم proxy.ts بعد حاليًا — استمر بـ middleware.ts لحد ما يُضاف الدعم.
كيف أمرر بيانات من proxy.ts إلى مكونات React؟
ما تقدر تمرر البيانات مباشرة. لكن عندك ثلاث طرق: (1) تعيّن ترويسات مخصصة عبر NextResponse.next({ request: { headers } }) وتقرأها في Server Components باستخدام دالة headers()، أو (2) تعيّن ملفات تعريف ارتباط عبر response.cookies.set()، أو (3) تعيد الكتابة إلى مسار يحتوي على معلمات استعلام.
هل proxy.ts مناسب لتسجيل الأحداث (Logging) والمراقبة؟
بالتأكيد. البروكسي مكان ممتاز لتسجيل معلومات الطلبات — المسار، الطريقة (Method)، عنوان IP، وقت الاستجابة. ومع Node.js Runtime في Next.js 16، تقدر تستخدم أي مكتبة تسجيل مثل winston أو pino. بس تأكد إن عمليات التسجيل تكون سريعة وغير متزامنة عشان ما تبطّئ الاستجابة.