Next.js Middleware Rehberi: Edge Runtime, Kimlik Doğrulama ve İleri Düzey Kalıplar

Next.js Middleware'in Edge Runtime üzerinde nasıl çalıştığını, JWT doğrulama, i18n, hız sınırlama ve A/B testi kalıplarını pratik örneklerle keşfedin. CVE-2025-29927 güvenlik açığından çıkarılan dersler ve derinlemesine savunma prensipleri.

Giriş

Bir HTTP isteği sunucuya ulaşmadan önce araya giren ve karar veren bir katman düşünün. Kimlik doğrulama mı yapılacak? Kullanıcı doğru sayfaya mı yönlendirilecek? Belki bir güvenlik başlığı eklenmeli? İşte Next.js'te bu katmana Middleware deniyor ve açıkçası, modern web uygulamalarında bu mekanizmayı anlamadan sağlam bir proje inşa etmek oldukça zor.

Middleware, App Router mimarisinde her istek için devreye girer, Edge Runtime üzerinde çalışır ve istekleri yönlendirme, yeniden yazma, başlık ekleme veya doğrudan yanıt dönme gibi işlemleri yapmanızı sağlar.

Bu rehberde temellerden başlayıp ileri düzey kalıplara kadar ilerleyeceğiz. Kimlik doğrulama, uluslararasılaştırma, hız sınırlama, A/B testi, coğrafi konum tabanlı yönlendirme gibi gerçek dünya senaryolarını pratik kod örnekleriyle ele alacağız. Bir de 2025'te keşfedilen ve ciddi yankı uyandıran CVE-2025-29927 güvenlik açığına bakacağız — çünkü bu olay, middleware güvenliği hakkında herkesin bilmesi gereken dersler içeriyor.

Eğer daha önce Next.js Server Actions hakkındaki yazımızı okuduysanız, Middleware'in sunucu tarafı mantığını nasıl tamamladığını daha kolay kavrayacaksınız. Server Actions veri mutasyonu için harika bir çözüm sunarken, Middleware isteğin yaşam döngüsünün en başında devreye girerek güvenlik ve yönlendirme katmanı oluşturur.

Middleware Nedir ve Nasıl Çalışır?

En basit haliyle Middleware, bir istek (request) ile bir yanıt (response) arasına oturan bir kod parçasıdır. Next.js'te sayfa veya API route'u render edilmeden önce çalışır. Yani her gelen isteği inceleyebilir, değiştirebilir veya tamamen engelleyebilirsiniz.

Edge Runtime Üzerinde Çalışma

Next.js Middleware'in en dikkat çekici yanı Edge Runtime üzerinde çalışmasıdır. Edge Runtime, geleneksel Node.js runtime'ından çok daha hafif ve hızlı bir ortam. Vercel gibi platformlarda kullanıcıya en yakın veri merkezinde çalışır, böylece gecikme süresi minimuma iner.

Edge Runtime'ın avantajları:

  • Düşük gecikme: Kullanıcıya coğrafi olarak yakın çalışır
  • Hızlı soğuk başlatma: Milisaniyeler içinde ayağa kalkar
  • Küçük boyut: Minimum bellek tüketir
  • Web Standartları API'leri: Fetch API, Web Crypto API gibi standart API'leri kullanır

Ama tabii bu avantajların bir bedeli var. Edge Runtime, Node.js'in tüm API'lerini desteklemiyor. Mesela fs (dosya sistemi), child_process veya bazı crypto fonksiyonları Edge Runtime'da kullanılamaz. Bu sınırlamalara ileride daha detaylı değineceğiz.

Çalışma Akışı

Bir istek geldiğinde Next.js'teki akış kabaca şöyle işler:

  1. İstek sunucuya ulaşır
  2. Middleware çalışır (matcher eşleşiyorsa)
  3. Middleware karar verir: devam et, yönlendir, yeniden yaz veya doğrudan yanıt dön
  4. Devam edilirse ilgili sayfa veya API route'u render edilir
  5. Yanıt istemciye döner

İlk Middleware'inizi Oluşturma

Next.js'te middleware oluşturmak aslında oldukça basit. Projenizin kök dizininde (veya src/ klasörü kullanıyorsanız src/ içinde) bir middleware.ts dosyası oluşturmanız yeterli.

Önemli not: Next.js 16 ile birlikte middleware.ts dosya adı proxy.ts olarak değişti. Next.js 15 ve öncesinde middleware.ts, Next.js 16 ve sonrasında ise proxy.ts kullanmanız gerekiyor. Bu makaledeki örnekler her iki sürüm için de geçerli — sadece dosya adını uygun şekilde değiştirmeniz yeterli.

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

// Middleware fonksiyonu: her eşleşen istek için çalışır
export function middleware(request: NextRequest) {
  // İsteğin URL'ini konsola yazdır
  console.log('İstek geldi:', request.nextUrl.pathname);

  // İsteğe özel bir başlık ekleyerek devam et
  const response = NextResponse.next();
  response.headers.set('x-middleware-cache', 'no-cache');

  return response;
}

// Hangi yolların middleware'den geçeceğini belirle
export const config = {
  matcher: [
    // Tüm sayfa isteklerini eşleştir, statik dosyaları hariç tut
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

Bu basit örnekte üç temel kavram var:

  • NextRequest: Gelen istek hakkında bilgi içerir (URL, başlıklar, çerezler, coğrafi konum vb.)
  • NextResponse: Yanıt oluşturmak veya isteği değiştirmek için kullanılır
  • config.matcher: Middleware'in hangi yollar için çalışacağını belirler

Temel Yanıt Türleri

Middleware'den dönebileceğiniz temel yanıt türlerini bir görelim:

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

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

  // 1. İsteği olduğu gibi devam ettir
  if (pathname === '/herkese-acik') {
    return NextResponse.next();
  }

  // 2. Başka bir sayfaya yönlendir (redirect)
  if (pathname === '/eski-sayfa') {
    return NextResponse.redirect(new URL('/yeni-sayfa', request.url));
  }

  // 3. İsteği farklı bir sayfaya yeniden yaz (rewrite)
  // URL değişmez ama farklı içerik sunulur
  if (pathname === '/deneysel') {
    return NextResponse.rewrite(new URL('/deneysel-v2', request.url));
  }

  // 4. Doğrudan JSON yanıt dön
  if (pathname === '/api/durum') {
    return NextResponse.json(
      { durum: 'aktif', zaman: new Date().toISOString() },
      { status: 200 }
    );
  }

  return NextResponse.next();
}

Matcher Yapılandırması

Matcher yapılandırması, middleware'inizin hangi istekler için çalışacağını belirler. Doğru yapılandırılmış bir matcher gereksiz middleware çağrılarını engeller ve uygulamanızın performansını doğrudan etkiler. Yani bu kısmı atlamayın.

Temel Matcher Desenleri

// Basit yol eşleştirme
export const config = {
  matcher: '/hakkimizda',
};

// Birden fazla yol eşleştirme
export const config = {
  matcher: ['/hakkimizda', '/iletisim', '/blog'],
};

// Joker karakter kullanarak eşleştirme
export const config = {
  matcher: '/blog/:path*', // /blog, /blog/yazi-1, /blog/kategori/yazi-2 vb.
};

Regex Desenleri ve Negatif Öngörüş (Negative Lookahead)

Daha karmaşık eşleştirme gereksinimleri için regex desenleri kullanabilirsiniz. En yaygın kullanım, statik dosyaları ve belirli yolları hariç tutmak:

export const config = {
  matcher: [
    // Statik dosyaları ve API rotalarını hariç tutan genel desen
    // Negatif öngörüş (negative lookahead) kullanır
    '/((?!api|_next/static|_next/image|favicon\\.ico|robots\\.txt|sitemap\\.xml).*)',
  ],
};

Bu desen şöyle çalışıyor:

  • (?!...) — Negatif öngörüş: belirtilen desenlerin başlamadığı yolları eşleştirir
  • api — API rotalarını hariç tutar
  • _next/static — Statik varlıkları (JS, CSS dosyaları) hariç tutar
  • _next/image — Optimize edilmiş görselleri hariç tutar
  • favicon\\.ico — Favicon dosyasını hariç tutar

Koşullu Eşleştirme

Matcher dışında, middleware fonksiyonunun içinde de koşullu mantık kurabilirsiniz:

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

  // Yalnızca /panel altındaki rotalar için kimlik doğrulama kontrolü
  if (pathname.startsWith('/panel')) {
    const token = request.cookies.get('oturum-token');
    if (!token) {
      return NextResponse.redirect(new URL('/giris', request.url));
    }
  }

  // Yalnızca /api rotaları için hız sınırlama
  if (pathname.startsWith('/api')) {
    // Hız sınırlama mantığı
  }

  return NextResponse.next();
}

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

Kimlik Doğrulama ve Yetkilendirme

Middleware'in en yaygın kullanım alanlarından biri kimlik doğrulama ve yetkilendirme kontrolleri. Kullanıcının oturum durumunu kontrol ederek korumalı sayfalara erişimi yönetebilirsiniz. Hadi birkaç farklı yaklaşıma bakalım.

JWT Token Doğrulama

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

// JWT gizli anahtarını çevre değişkeninden al
const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || 'gizli-anahtar'
);

// Korumalı rotaların listesi
const KORUMALI_ROTALAR = ['/panel', '/profil', '/ayarlar'];

// Yalnızca yönetici erişimi gerektiren rotalar
const YONETICI_ROTALARI = ['/panel/yonetim', '/panel/kullanicilar'];

async function tokenDogrula(token: string) {
  try {
    // jose kütüphanesi Edge Runtime ile uyumludur
    const { payload } = await jwtVerify(token, JWT_SECRET);
    return payload;
  } catch (hata) {
    return null;
  }
}

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

  // Korumalı rota kontrolü
  const korunmaliRota = KORUMALI_ROTALAR.some((rota) =>
    pathname.startsWith(rota)
  );

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

  // Çerezden veya Authorization başlığından token al
  const token =
    request.cookies.get('auth-token')?.value ||
    request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    // Giriş sayfasına yönlendir, geri dönüş URL'ini kaydet
    const girisUrl = new URL('/giris', request.url);
    girisUrl.searchParams.set('geriDonus', pathname);
    return NextResponse.redirect(girisUrl);
  }

  // Token doğrulama
  const payload = await tokenDogrula(token);

  if (!payload) {
    // Geçersiz token: çerezi temizle ve girişe yönlendir
    const yanit = NextResponse.redirect(new URL('/giris', request.url));
    yanit.cookies.delete('auth-token');
    return yanit;
  }

  // Yönetici rota kontrolü
  const yoneticiRotasi = YONETICI_ROTALARI.some((rota) =>
    pathname.startsWith(rota)
  );

  if (yoneticiRotasi && payload.rol !== 'yonetici') {
    // Yetkisiz erişim: 403 sayfasına yönlendir
    return NextResponse.redirect(new URL('/yetkisiz', request.url));
  }

  // Kullanıcı bilgisini başlıklara ekle (downstream kullanım için)
  const yanit = NextResponse.next();
  yanit.headers.set('x-kullanici-id', payload.sub as string);
  yanit.headers.set('x-kullanici-rol', payload.rol as string);

  return yanit;
}

Oturum Çerezi ile Doğrulama

JWT yerine basit bir oturum çerezi yaklaşımı da kullanabilirsiniz:

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

export function middleware(request: NextRequest) {
  const oturumCerezi = request.cookies.get('oturum-id');
  const { pathname } = request.nextUrl;

  // Giriş sayfasındaki kullanıcı zaten oturum açmışsa panele yönlendir
  if (pathname === '/giris' && oturumCerezi) {
    return NextResponse.redirect(new URL('/panel', request.url));
  }

  // Korumalı sayfalarda oturum kontrolü
  if (pathname.startsWith('/panel') && !oturumCerezi) {
    const girisUrl = new URL('/giris', request.url);
    girisUrl.searchParams.set('geriDonus', pathname);
    return NextResponse.redirect(girisUrl);
  }

  return NextResponse.next();
}

Önemli uyarı: Middleware'deki kimlik doğrulama yalnızca bir ilk savunma hattıdır. Birazdan göreceğiniz gibi, sadece middleware'e güvenmek ciddi güvenlik açıklarına yol açabiliyor. Her zaman veri erişim katmanında da doğrulama yapın.

CVE-2025-29927 Güvenlik Açığı ve Dersler

Mart 2025'te keşfedilen CVE-2025-29927, Next.js middleware güvenliği konusunda adeta bir deprem etkisi yarattı. Bu güvenlik açığı, saldırganların middleware kontrollerini tamamen atlayabilmesine olanak tanıyordu. Evet, tamamen.

Açığın Teknik Detayları

Next.js, dahili olarak x-middleware-subrequest adında bir HTTP başlığı kullanıyordu. Bu başlık, middleware'in kendi kendini çağırmasından kaynaklanan sonsuz döngüleri önlemek için tasarlanmıştı. Başlık mevcut olduğunda Next.js, middleware'i atlayıp isteği doğrudan hedef rotaya yönlendiriyordu.

Sorun ne miydi? Bu başlık dışarıdan gelen isteklerde filtrelenmiyordu. Yani bir saldırgan, isteğine basitçe bu başlığı ekleyerek tüm middleware kontrollerini — kimlik doğrulama, yetkilendirme, CSP başlıkları, hız sınırlama dahil — bypass edebiliyordu. Düşünün, tek bir HTTP başlığı ile tüm güvenlik duvarınız çöküyor.

# Saldırı örneği (ETKİLENEN SÜRÜMLERDE)
# Bu başlığı ekleyerek middleware tamamen atlanabiliyordu
curl -H "x-middleware-subrequest: middleware" https://hedef-site.com/panel/gizli-sayfa

Etkilenen Sürümler

  • Next.js 12.x: 12.3.5 öncesi tüm sürümler
  • Next.js 13.x: 13.5.9 öncesi tüm sürümler
  • Next.js 14.x: 14.2.25 öncesi tüm sürümler

Azaltma Stratejileri

  1. Hemen güncelleme yapın: Next.js'i yamanın içeren en son sürüme yükseltin
  2. Başlığı engelleyin: Eğer hemen güncelleyemiyorsanız, ters proxy (Nginx, Cloudflare vb.) seviyesinde x-middleware-subrequest başlığını dış isteklerden temizleyin
  3. Çok katmanlı doğrulama uygulayın: Middleware'e asla tek güvenlik katmanı olarak güvenmeyin
# Nginx ile x-middleware-subrequest başlığını temizleme
# nginx.conf içinde
location / {
    # Dışarıdan gelen isteklerdeki tehlikeli başlığı kaldır
    proxy_set_header x-middleware-subrequest "";
    proxy_pass http://nextjs-uygulama:3000;
}

Çıkarılan Dersler

Bu güvenlik açığından çıkarılması gereken en kritik ders aslında çok net: Middleware'i asla tek güvenlik katmanı olarak kullanmayın. Middleware, güvenlik mimarinizin sadece ilk katmanı olmalı. Gerçek kimlik doğrulama ve yetkilendirme kontrolleri, mutlaka veri erişim katmanında da yapılmalı.

// ❌ YANLIŞ: Yalnızca middleware'e güvenme
// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');
  if (!token) return NextResponse.redirect('/giris');
  return NextResponse.next(); // Token varsa geçir, başka doğrulama yok
}

// ✔ DOĞRU: Veri erişim katmanında da doğrulama
// lib/veri-erisim.ts
export async function kullaniciBilgisiGetir(kullaniciId: string) {
  // Oturumu bağımsız olarak doğrula
  const oturum = await oturumuDogrula();

  if (!oturum || oturum.kullaniciId !== kullaniciId) {
    throw new Error('Yetkisiz erişim');
  }

  // Ancak doğrulama başarılıysa veritabanına eriş
  return await db.kullanicilar.findUnique({
    where: { id: kullaniciId },
  });
}

Bu yaklaşım, güvenlik dünyasında "derinlemesine savunma" (defense in depth) olarak bilinen bir prensip. Bir katman aşılsa bile diğer katmanlar korumayı sürdürür. Açıkçası, CVE-2025-29927 bu prensibin neden bu kadar önemli olduğunun en somut kanıtlarından biri.

Uluslararasılaştırma (i18n) Yönlendirmesi

Çok dilli uygulamalarda middleware, kullanıcıyı tercih ettiği dile yönlendirmek için biçilmiş kaftan. Accept-Language başlığını okuyarak, çerezleri kontrol ederek veya URL yapısına göre doğru dil sürümünü sunabilirsiniz.

Temel Dil Algılama ve Yönlendirme

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

// Desteklenen diller
const DESTEKLENEN_DILLER = ['tr', 'en', 'de', 'fr'];
const VARSAYILAN_DIL = 'tr';

// Accept-Language başlığını ayrıştırarak tercih edilen dili bul
function tercihEdilenDiliBul(acceptLanguage: string | null): string {
  if (!acceptLanguage) return VARSAYILAN_DIL;

  // Accept-Language başlığını ayrıştır (örn: "tr-TR,tr;q=0.9,en;q=0.8")
  const diller = acceptLanguage
    .split(',')
    .map((dil) => {
      const [kod, kalite] = dil.trim().split(';q=');
      return {
        kod: kod.split('-')[0].toLowerCase(), // "tr-TR" -> "tr"
        kalite: kalite ? parseFloat(kalite) : 1.0,
      };
    })
    .sort((a, b) => b.kalite - a.kalite);

  // Desteklenen dillerle eşleştir
  for (const dil of diller) {
    if (DESTEKLENEN_DILLER.includes(dil.kod)) {
      return dil.kod;
    }
  }

  return VARSAYILAN_DIL;
}

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

  // Zaten bir dil öneki varsa devam et
  const mevcutDil = DESTEKLENEN_DILLER.find(
    (dil) => pathname.startsWith(`/${dil}/`) || pathname === `/${dil}`
  );

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

  // Statik dosyaları atla
  if (
    pathname.startsWith('/_next') ||
    pathname.includes('.') // dosya uzantısı olan istekler
  ) {
    return NextResponse.next();
  }

  // Çerezden dil tercihini kontrol et
  const cerezDili = request.cookies.get('tercih-edilen-dil')?.value;

  // Dili belirle: önce çerez, sonra Accept-Language başlığı
  const dil =
    cerezDili && DESTEKLENEN_DILLER.includes(cerezDili)
      ? cerezDili
      : tercihEdilenDiliBul(request.headers.get('accept-language'));

  // Kullanıcıyı doğru dil yoluna yönlendir
  const yeniUrl = new URL(`/${dil}${pathname}`, request.url);
  yeniUrl.search = request.nextUrl.search; // Sorgu parametrelerini koru

  return NextResponse.redirect(yeniUrl);
}

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

next-intl ile Entegrasyon

Daha kapsamlı bir i18n çözümü arıyorsanız, next-intl kütüphanesi yerleşik middleware desteğiyle işinizi çok kolaylaştırır:

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

// next-intl middleware'ini oluştur
export default createMiddleware(routing);

export const config = {
  // next-intl'in önerdiği matcher deseni
  matcher: ['/', '/(tr|en|de|fr)/:path*'],
};
// i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  // Desteklenen diller
  locales: ['tr', 'en', 'de', 'fr'],

  // Varsayılan dil
  defaultLocale: 'tr',

  // Varsayılan dil için URL önekini gizle
  // /tr/hakkimizda yerine /hakkimizda gösterilir
  localePrefix: 'as-needed',
});

Bu yapılandırmayla App Router'daki klasör yapınız app/[locale]/ şeklinde olmalı. Her sayfa otomatik olarak locale parametresini alır ve ilgili çeviri dosyasını kullanır.

Hız Sınırlama (Rate Limiting)

API'lerinizi kötüye kullanımdan korumak istiyorsanız, middleware seviyesinde hız sınırlama uygulamak mantıklı bir seçenek. Edge Runtime'da çalıştığı için istekler en erken aşamada filtreleniyor — bu da sunucu kaynaklarınızı gereksiz yükten korur.

Upstash Redis ile Kayan Pencere Algoritması

Edge Runtime'da kullanabileceğiniz en popüler çözüm Upstash Redis. Neden mi? Upstash, HTTP tabanlı bir Redis hizmeti sunduğu için Edge Runtime ile sorunsuz çalışıyor (TCP bağlantısı gerektirmiyor, ki bu Edge'de zaten mümkün değil).

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

// Upstash Redis istemcisini oluştur
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Kayan pencere (sliding window) algoritmasıyla hız sınırlayıcı
const hizSinirla = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(
    20, // Maksimum istek sayısı
    '60 s' // Zaman penceresi: 60 saniye
  ),
  analytics: true, // Upstash Dashboard'da analitik görüntüle
  prefix: 'uygulama:hiz-sinir', // Redis anahtar öneki
});

// IP adresini güvenli bir şekilde al
function ipAdresiAl(request: NextRequest): string {
  const xForwardedFor = request.headers.get('x-forwarded-for');
  if (xForwardedFor) {
    return xForwardedFor.split(',')[0].trim();
  }
  return request.headers.get('x-real-ip') || '127.0.0.1';
}

export async function middleware(request: NextRequest) {
  // Yalnızca API rotaları için hız sınırlama uygula
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next();
  }

  const ip = ipAdresiAl(request);
  const { success, limit, remaining, reset } = await hizSinirla.limit(ip);

  // Hız sınırı bilgilerini yanıt başlıklarına ekle
  const yanit = success
    ? NextResponse.next()
    : NextResponse.json(
        {
          hata: 'Hız sınırı aşıldı',
          mesaj: 'Lütfen daha sonra tekrar deneyin.',
        },
        { status: 429 }
      );

  yanit.headers.set('X-RateLimit-Limit', limit.toString());
  yanit.headers.set('X-RateLimit-Remaining', remaining.toString());
  yanit.headers.set('X-RateLimit-Reset', reset.toString());

  return yanit;
}

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

Rota Bazında Farklı Sınırlar

Her API endpoint'inin aynı hız sınırına ihtiyacı yok. Örneğin giriş endpoint'ine brute force koruması için çok daha sıkı bir limit koyabilirsiniz:

// Farklı rotalar için farklı hız sınırlayıcılar
const sinirlar = {
  // Genel API: dakikada 60 istek
  genel: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(60, '60 s'),
    prefix: 'hs:genel',
  }),
  // Kimlik doğrulama: dakikada 5 istek (brute force korunması)
  giris: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(5, '60 s'),
    prefix: 'hs:giris',
  }),
  // Dosya yükleme: saatte 10 istek
  yukleme: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(10, '3600 s'),
    prefix: 'hs:yukleme',
  }),
};

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

  // Rotaya göre uygun sınırlayıcıyı seç
  let sinir;
  if (pathname.startsWith('/api/giris') || pathname.startsWith('/api/kayit')) {
    sinir = sinirlar.giris;
  } else if (pathname.startsWith('/api/yukleme')) {
    sinir = sinirlar.yukleme;
  } else {
    sinir = sinirlar.genel;
  }

  const sonuc = await sinir.limit(ip);

  if (!sonuc.success) {
    return NextResponse.json(
      { hata: 'Hız sınırı aşıldı' },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

A/B Testi ve Özellik Bayrakları

Middleware, A/B testi ve özellik bayrakları (feature flags) için gerçekten ideal bir katman. Çerez tabanlı kullanıcı segmentasyonu ve rewrite mekanizmasıyla, kullanıcılar URL'de hiçbir fark görmeden farklı sayfa varyasyonlarıyla karşılaşabilir.

Çerez Tabanlı A/B Testi

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

// A/B test grupları
const TEST_GRUPLARI = ['kontrol', 'deney-a', 'deney-b'] as const;
type TestGrubu = (typeof TEST_GRUPLARI)[number];

// Kullanıcıyı rastgele bir gruba ata
function rastgeleGrupAta(): TestGrubu {
  const rastgele = Math.random();
  if (rastgele < 0.33) return 'kontrol';
  if (rastgele < 0.66) return 'deney-a';
  return 'deney-b';
}

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

  // Yalnızca A/B test uygulanan sayfalarda çalış
  if (pathname !== '/fiyatlandirma') {
    return NextResponse.next();
  }

  // Mevcut grup atasını çerezden kontrol et
  let grup = request.cookies.get('ab-fiyat-testi')?.value as
    | TestGrubu
    | undefined;

  // Eğer çerez yoksa yeni grup ata
  if (!grup || !TEST_GRUPLARI.includes(grup)) {
    grup = rastgeleGrupAta();
  }

  // Gruba göre farklı sayfaya yeniden yaz (rewrite)
  // URL değişmez, kullanıcı hala /fiyatlandirma görür
  const yanit = NextResponse.rewrite(
    new URL(`/fiyatlandirma/${grup}`, request.url)
  );

  // Grup bilgisini çereze kaydet (30 gün geçerli)
  yanit.cookies.set('ab-fiyat-testi', grup, {
    maxAge: 60 * 60 * 24 * 30,
    httpOnly: true,
    sameSite: 'lax',
    path: '/',
  });

  return yanit;
}

Özellik Bayrakları ile Koşullu Yönlendirme

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

// Özellik bayrakları yapılandırması
const OZELLIK_BAYRAKLARI: Record<string, {
  aktif: boolean;
  yuzdesi: number; // Kullanıcıların yüzde kaçı görecek
}> = {
  'yeni-arayuz': { aktif: true, yuzdesi: 50 },
  'gelismis-arama': { aktif: true, yuzdesi: 25 },
  'karanlik-mod': { aktif: false, yuzdesi: 0 },
};

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

  // Her özellik bayrağını kontrol et
  for (const [bayrak, ayar] of Object.entries(OZELLIK_BAYRAKLARI)) {
    if (!ayar.aktif) continue;

    const cerezAdi = `ff-${bayrak}`;
    let deger = request.cookies.get(cerezAdi)?.value;

    // İlk ziyarette bayrağı ata
    if (!deger) {
      deger = Math.random() * 100 < ayar.yuzdesi ? 'acik' : 'kapali';
      yanit.cookies.set(cerezAdi, deger, {
        maxAge: 60 * 60 * 24 * 7,
        path: '/',
      });
    }

    // Bayrak değerini başlığa ekle (sayfa bileşenlerinde kullanmak için)
    yanit.headers.set(`x-ff-${bayrak}`, deger);
  }

  // Özellik bayrağına göre yeniden yazma örneği
  if (
    pathname === '/arama' &&
    request.cookies.get('ff-gelismis-arama')?.value === 'acik'
  ) {
    return NextResponse.rewrite(new URL('/arama/v2', request.url));
  }

  return yanit;
}

Bu yapıyla app/fiyatlandirma/kontrol/page.tsx, app/fiyatlandirma/deney-a/page.tsx ve app/fiyatlandirma/deney-b/page.tsx dosyalarını oluşturarak her gruba farklı bir deneyim sunabilirsiniz. Kullanıcı URL'de hiçbir değişiklik görmez — her zaman /fiyatlandirma adresini görür. Oldukça zarif bir çözüm, değil mi?

Coğrafi Konum Tabanlı Yönlendirme

Next.js middleware'i, gelen istekle birlikte coğrafi konum bilgisi sağlar. Bu bilgiyi kullanarak kullanıcıları ülke veya bölgelerine göre farklı içeriklere yönlendirebilirsiniz.

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

// Ülke koduna göre dil eşlemesi
const ULKE_DIL_ESLEME: Record<string, string> = {
  TR: 'tr',
  DE: 'de',
  AT: 'de',
  CH: 'de',
  US: 'en',
  GB: 'en',
  FR: 'fr',
};

// Bölgesel fiyatlandırma bölgeleri
const FIYAT_BOLGELERI: Record<string, string> = {
  TR: 'turkiye',
  US: 'amerika',
  GB: 'avrupa',
  DE: 'avrupa',
  FR: 'avrupa',
  JP: 'asya',
};

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

  // Coğrafi konum bilgisini al
  // Not: Bu bilgi Vercel gibi platformlarda otomatik doldurulur
  // Yerel geliştirmede boş olabilir
  const ulke = request.geo?.country || 'TR';
  const sehir = request.geo?.city || 'bilinmiyor';

  // Coğrafi konum bilgilerini başlıklara ekle
  const yanit = NextResponse.next();
  yanit.headers.set('x-kullanici-ulke', ulke);
  yanit.headers.set('x-kullanici-sehir', sehir);

  // Fiyatlandırma sayfası için bölgesel yönlendirme
  if (pathname === '/fiyatlandirma') {
    const fiyatBolgesi = FIYAT_BOLGELERI[ulke] || 'varsayilan';
    return NextResponse.rewrite(
      new URL(`/fiyatlandirma/${fiyatBolgesi}`, request.url)
    );
  }

  // Ülkeye göre otomatik dil yönlendirmesi
  if (pathname === '/') {
    const tercihEdilenDil = ULKE_DIL_ESLEME[ulke] || 'en';
    const mevcutDilTercihi = request.cookies.get('dil-tercihi')?.value;

    // Kullanıcının manuel seçimi varsa onu kullan
    if (!mevcutDilTercihi) {
      return NextResponse.redirect(
        new URL(`/${tercihEdilenDil}`, request.url)
      );
    }
  }

  return yanit;
}

Dikkat: request.geo özelliği yalnızca Vercel gibi bunu destekleyen platformlarda çalışır. Kendi sunucunuzda çalıştırıyorsanız, coğrafi konum bilgisini bir IP geolocation servisi (mesela MaxMind GeoIP) aracılığıyla kendiniz eklemeniz gerekecektir.

Edge Runtime Sınırlamaları ve En İyi Uygulamalar

Edge Runtime performans açısından harika, ama bazı önemli sınırlamalarını bilmeden yola çıkarsanız saatlerce hata ayıklamakla uğraşabilirsiniz. Şimdi bunlara bir göz atalım.

Kullanılamayan API'ler ve Modüller

  • Dosya sistemi (fs): Dosya okuma/yazma işlemleri yapılamaz
  • Node.js yerleşik modülleri: path, os, child_process, net, tls gibi modüller kullanılamaz
  • Node.js crypto modülü: Bunun yerine Web Crypto API (crypto.subtle) kullanılmalı
  • Yerel (native) modüller: C/C++ ile yazılmış yerel eklentiler çalışmaz
  • TCP/UDP soketleri: Doğrudan soket bağlantıları kurulamaz (bu yüzden geleneksel Redis istemcileri çalışmaz; Upstash gibi HTTP tabanlı alternatifler şart)

Boyut Sınırlamaları

Middleware kodu ve bağımlılıkları, sıkıştırılmamış haliyle genellikle 1 MB sınırına tabi (barındırma platformuna göre değişebilir). Bu yüzden büyük kütüphaneleri middleware'e dahil etmekten kaçının.

En İyi Uygulamalar

// ❌ YANLIŞ: Ağır işlemleri middleware'de yapma
export async function middleware(request: NextRequest) {
  // Veritabanından tüm kullanıcıları çekme - ÇOK YAVAŞ!
  const kullanicilar = await fetch('https://api.ornek.com/tum-kullanicilar');
  const veri = await kullanicilar.json();

  // Karmaşık veri işleme - EDGE RUNTIME İÇİN ÇOK AĞIR!
  const islenmisVeri = veri.map((k: any) => /* karmaşık hesaplama */ k);

  return NextResponse.next();
}

// ✔ DOĞRU: Middleware'i hafif tutun
export async function middleware(request: NextRequest) {
  // Yalnızca basit bir token kontrolü
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/giris', request.url));
  }

  // Hafif bir doğrulama (tam doğrulama sayfa seviyesinde yapılsın)
  try {
    const { payload } = await jwtVerify(token, secret);
    const yanit = NextResponse.next();
    yanit.headers.set('x-kullanici-id', payload.sub as string);
    return yanit;
  } catch {
    return NextResponse.redirect(new URL('/giris', request.url));
  }
}

Middleware'i hafif tutmak için şu prensiplere dikkat edin:

  1. Minimum dış çağrı: Mümkünse dış API çağrılarından kaçının. Zorunluysa tek bir hızlı çağrı yapın.
  2. Küçük bağımlılıklar: Yalnızca Edge Runtime ile uyumlu, küçük kütüphaneler kullanın (jose, @upstash/redis gibi).
  3. Erken çıkış: Eşleşmeyen istekler için mümkün olan en erken noktada NextResponse.next() dönün.
  4. Önbellekleme: Mümkün olan her yerde önbellekleme mekanizmaları kullanın.

Güvenlik En İyi Uygulamaları

CVE-2025-29927 deneyiminden sonra middleware güvenliği konusunu ciddiye almamak elde değil. Gelin, alınması gereken önlemleri kapsamlı bir şekilde ele alalım.

Derinlemesine Savunma (Defense in Depth)

Güvenli bir uygulama oluşturmanın altın kuralı: tek bir katmana güvenmeyin. Birden fazla savunma katmanı oluşturun ki biri aşılsa diğeri devreye girsin.

// 1. KATMAN: Middleware - İlk savunma hattı
// middleware.ts
export async function middleware(request: NextRequest) {
  // Hızlı token varlık kontrolü
  const token = request.cookies.get('auth-token')?.value;
  if (!token && request.nextUrl.pathname.startsWith('/panel')) {
    return NextResponse.redirect(new URL('/giris', request.url));
  }
  return NextResponse.next();
}

// 2. KATMAN: Sayfa seviyesi - Sunucu bileşeninde doğrulama
// app/panel/page.tsx
import { redirect } from 'next/navigation';
import { oturumuDogrula } from '@/lib/auth';

export default async function PanelSayfasi() {
  const oturum = await oturumuDogrula();
  if (!oturum) {
    redirect('/giris');
  }

  return <div>Merhaba, {oturum.kullaniciAdi}</div>;
}

// 3. KATMAN: Veri erişim katmanı - En kritik savunma
// lib/veri-erisim.ts
import { oturumuDogrula } from '@/lib/auth';

export async function panelVerileriniGetir() {
  const oturum = await oturumuDogrula();
  if (!oturum) {
    throw new Error('Yetkisiz erişim');
  }

  return await db.panelVerileri.findMany({
    where: { kullaniciId: oturum.kullaniciId },
  });
}

Güvenlik Başlıkları Ekleme

Middleware, güvenlik başlıklarını eklemek ve potansiyel olarak tehlikeli başlıkları temizlemek için birebir:

export function middleware(request: NextRequest) {
  const yanit = NextResponse.next();

  // Güvenlik başlıkları ekle
  yanit.headers.set('X-Content-Type-Options', 'nosniff');
  yanit.headers.set('X-Frame-Options', 'DENY');
  yanit.headers.set('X-XSS-Protection', '1; mode=block');
  yanit.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  yanit.headers.set(
    'Strict-Transport-Security',
    'max-age=63072000; includeSubDomains; preload'
  );

  // Content Security Policy
  yanit.headers.set(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' blob: data:",
      "font-src 'self'",
      "connect-src 'self'",
      "frame-ancestors 'none'",
    ].join('; ')
  );

  // Dahili başlıkların dışarı sızmamasını sağla
  yanit.headers.delete('x-powered-by');
  yanit.headers.delete('server');

  return yanit;
}

CORS Yönetimi

// İzin verilen kaynaklar
const IZIN_VERILEN_KAYNAKLAR = [
  'https://www.ornek.com',
  'https://uygulama.ornek.com',
];

export function middleware(request: NextRequest) {
  // Yalnızca API rotaları için CORS uygula
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next();
  }

  const kaynak = request.headers.get('origin') || '';
  const izinliMi = IZIN_VERILEN_KAYNAKLAR.includes(kaynak);

  // Preflight (OPTIONS) isteklerini işle
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': izinliMi ? kaynak : '',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  // Normal istekler için CORS başlıkları ekle
  const yanit = NextResponse.next();
  if (izinliMi) {
    yanit.headers.set('Access-Control-Allow-Origin', kaynak);
    yanit.headers.set('Access-Control-Allow-Credentials', 'true');
  }

  return yanit;
}

Kapsamlı Middleware Örneği

Şimdi tüm bu güvenlik uygulamalarını bir araya getiren kapsamlı bir örneğe bakalım. Bu, gerçek bir projede kullanabileceğiniz türden bir middleware:

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

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

// Korumalı ve genel rotaları tanımla
const GENEL_ROTALAR = ['/giris', '/kayit', '/sifremi-unuttum', '/hakkimizda'];
const YONETICI_ROTALARI = ['/panel/yonetim'];

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

  // 1. Güvenlik başlıklarını her yanıta ekle
  yanit.headers.set('X-Content-Type-Options', 'nosniff');
  yanit.headers.set('X-Frame-Options', 'DENY');

  // 2. Genel rotalarda kimlik doğrulama gerekmez
  if (GENEL_ROTALAR.some((r) => pathname.startsWith(r))) {
    return yanit;
  }

  // 3. API rotaları için ek kontroller
  if (pathname.startsWith('/api')) {
    return yanit;
  }

  // 4. Korumalı rotalar için kimlik doğrulama
  if (pathname.startsWith('/panel') || pathname.startsWith('/profil')) {
    const token = request.cookies.get('auth-token')?.value;

    if (!token) {
      const girisUrl = new URL('/giris', request.url);
      girisUrl.searchParams.set('geriDonus', pathname);
      return NextResponse.redirect(girisUrl);
    }

    try {
      const { payload } = await jwtVerify(token, JWT_SECRET);

      // Yönetici rotaları için ek yetkilendirme
      if (
        YONETICI_ROTALARI.some((r) => pathname.startsWith(r)) &&
        payload.rol !== 'yonetici'
      ) {
        return NextResponse.redirect(new URL('/yetkisiz', request.url));
      }

      // Kullanıcı bilgilerini başlıklara ekle
      yanit.headers.set('x-kullanici-id', payload.sub as string);
      yanit.headers.set('x-kullanici-rol', payload.rol as string);
    } catch {
      // Geçersiz token
      const temizYanit = NextResponse.redirect(new URL('/giris', request.url));
      temizYanit.cookies.delete('auth-token');
      return temizYanit;
    }
  }

  return yanit;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon\\.ico|robots\\.txt|sitemap\\.xml).*)',
  ],
};

Sonuç

Next.js Middleware, modern web uygulamalarında güvenlik, performans ve kullanıcı deneyimi açısından gerçekten vazgeçilmez bir araç. Edge Runtime sayesinde düşük gecikme ile hızlı karar verme imkanı sunuyor ve uygulamanızın ilk savunma hattını oluşturuyor.

Bu rehberde neler ele aldık, kısaca özetleyelim:

  • Temel Kavramlar: Middleware'in ne olduğu, Edge Runtime üzerinde nasıl çalıştığı ve NextRequest/NextResponse API'leri
  • Matcher Yapılandırması: Regex desenleri, negatif öngörüş ve statik dosyaları hariç tutma yöntemleri
  • Kimlik Doğrulama: JWT token doğrulama, oturum yönetimi ve korumalı rota kalıpları
  • CVE-2025-29927: x-middleware-subrequest başlık atlama açığı ve neden sadece middleware'e güvenmemeniz gerektiği
  • Uluslararasılaştırma: Accept-Language başlığı ayrıştırma, çerez tabanlı dil tercihi ve next-intl entegrasyonu
  • Hız Sınırlama: Upstash Redis ile kayan pencere algoritması ve rota bazında farklı limitler
  • A/B Testi: Çerez tabanlı kullanıcı segmentasyonu ve özellik bayrakları
  • Coğrafi Konum: Ülke tabanlı yönlendirme ve bölgesel içerik sunma
  • Güvenlik: Derinlemesine savunma prensibi, güvenlik başlıkları ve CORS yönetimi

Son olarak birkaç tavsiye:

  1. Middleware'i hafif tutun. Ağır hesaplamalar ve veritabanı sorguları sunucu bileşenlerinde veya API rotalarında yapılmalı.
  2. Derinlemesine savunma uygulayın. Middleware yalnızca ilk filtre olmalı; gerçek yetkilendirme kontrolleri veri erişim katmanında da tekrarlanmalı.
  3. Next.js sürümünüzü güncel tutun. CVE-2025-29927 gibi güvenlik açıkları ancak güncelleme ile tamamen giderilebilir.
  4. Edge Runtime sınırlamalarını bilin. Kullandığınız kütüphanelerin Edge uyumlu olduğundan emin olun.
  5. Next.js 16'ya geçişte dosya adını güncelleyin. middleware.ts yerine proxy.ts kullanmanız gerekecek.

Middleware, doğru kullanıldığında Next.js uygulamalarınızın güvenliğini, performansını ve kullanıcı deneyimini ciddi anlamda iyileştirebilir. Ama her güçlü araç gibi, sorumlulukla ve en iyi uygulamalar doğrultusunda kullanılmalı. Derinlemesine savunma prensibini asla ihmal etmeyin — güvenliği tek bir katmana bırakmak yerine, uygulamanızın her seviyesinde doğrulama ve yetkilendirme kontrolleri uygulayın.

Yazar Hakkında Editorial Team

Our team of expert writers and editors.