Oluşturmadan Önce Ara Yazılım

Vercel'de dağıtılan geleneksel Next.js uygulamasında her istek önce CDN'ye ulaşır, ardından önbellekte değilse iletilir SSR sunucusuna. Edge ara yazılımı bu iki aşama arasında yer alır: SSR sunucusuna yönlendirilmeden önce Vercel ağının ucunda gerçekleştirilir ve yanıt önbellekten sunulmadan önce.

Bu konumlandırma kritiktir: ara katman yazılımı değiştirebilir istek, kullanıcıyı yönlendirmek, çerezleri veya başlıkları ayarlamak ve bunların hepsi sıfıra yakın soğuk başlatma ile V8 tabanlı bir çalışma zamanında gerçekleşir. Vercel önlemleri çoğu ara katman yazılımı için P50 < 10 ms gecikme uç bölgeler (100'den fazla küresel).

Ne Öğreneceksiniz

  • Next.js Uygulama Yönlendiricisinde ara yazılım yapısı ve dosya kuralı
  • Coğrafi konum: Ülkeye, bölgeye ve dile göre yönlendirme
  • Kalıcı çerezlerle istikrarlı A/B testi
  • Sunucuya gidiş-dönüş olmadan kenarda özellik bayrakları
  • Ara yazılımda hafif kimlik doğrulama
  • Node.js ile karşılaştırıldığında Vercel Edge Çalışma Zamanının Sınırlamaları
  • Yerel ara yazılım hata ayıklaması next dev

Dosya middleware.ts

Next.js'de (Uygulama Yönlendiricisi ve Sayfa Yönlendiricisi), ara katman yazılımı dosyada tanımlanır middleware.ts (o .js) proje kökünde. Dışa aktarılan işlev bir nesne alır NextRequest ve geri vermeli bir NextResponse:

// middleware.ts - struttura base

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

// La funzione middleware e chiamata per ogni richiesta che corrisponde al matcher
export function middleware(request: NextRequest): NextResponse {
  // request.nextUrl: URL della richiesta (con searchParams, pathname, etc.)
  // request.headers: header HTTP
  // request.cookies: cookie della richiesta
  // request.geo: geolocalizzazione (solo su Vercel, non in local dev)
  // request.ip: IP del client

  const response = NextResponse.next(); // Prosegui senza modifiche

  // Aggiunge un header custom alla risposta
  response.headers.set('X-Middleware-Applied', 'true');

  return response;
}

// Configura su quali path il middleware viene eseguito
// IMPORTANTE: specificare il matcher migliora le performance
// evitando l'esecuzione per asset statici (_next/static, immagini, etc.)
export const config = {
  matcher: [
    // Esegui su tutte le route eccetto quelle statiche e API
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Coğrafi Konum: Ülkeye ve Dile Göre Yönlendirme

Vercel her talebi konu üzerinden coğrafi bilgilerle zenginleştiriyor request.geo. Üretimde (yerel geliştirmede değil), bunlar mevcut ülke, bölge ve şehir:

// middleware.ts - redirect geografico per lingua

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

// Mappa paese -> locale preferito
const COUNTRY_LOCALE_MAP: Record<string, string> = {
  IT: 'it',
  DE: 'de',
  FR: 'fr',
  ES: 'es',
  PT: 'pt',
  BR: 'pt-BR',
  // Default: 'en' per tutti gli altri paesi
};

const SUPPORTED_LOCALES = ['en', 'it', 'de', 'fr', 'es', 'pt'];
const DEFAULT_LOCALE = 'en';

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

  // Non processare le route che gia hanno un locale nel path
  // Esempio: /it/dashboard non deve essere reindirizzato
  const pathLocale = pathname.split('/')[1];
  if (SUPPORTED_LOCALES.includes(pathLocale)) {
    return NextResponse.next();
  }

  // Determina il locale preferito
  // Priorita: 1) Cookie impostato dall'utente, 2) Paese IP, 3) Accept-Language, 4) Default
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
    return rewriteWithLocale(request, cookieLocale);
  }

  const country = request.geo?.country ?? 'US';
  const geoLocale = COUNTRY_LOCALE_MAP[country] ?? DEFAULT_LOCALE;

  // Verifica Accept-Language come fallback
  const acceptLanguage = request.headers.get('Accept-Language') ?? '';
  const browserLocale = parseAcceptLanguage(acceptLanguage, SUPPORTED_LOCALES);

  const finalLocale = geoLocale !== DEFAULT_LOCALE
    ? geoLocale
    : (browserLocale ?? DEFAULT_LOCALE);

  return rewriteWithLocale(request, finalLocale);
}

function rewriteWithLocale(request: NextRequest, locale: string): NextResponse {
  const url = request.nextUrl.clone();
  url.pathname = `/${locale}${url.pathname}`;
  // Rewrite (non redirect) per mantenere l'URL originale nel browser
  // L'utente vede /dashboard ma viene servita /it/dashboard
  return NextResponse.rewrite(url);
}

function parseAcceptLanguage(header: string, supported: string[]): string | null {
  // Parsa "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7"
  const languages = header
    .split(',')
    .map((lang) => {
      const [code, q] = lang.trim().split(';q=');
      return { code: code.split('-')[0].toLowerCase(), q: parseFloat(q ?? '1') };
    })
    .sort((a, b) => b.q - a.q);

  for (const { code } of languages) {
    if (supported.includes(code)) return code;
  }
  return null;
}

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

Çerezlerle Kararlı A/B Testi

Uçta A/B testi her kullanıcının kendi grubunda kalmasını gerektirir oturum boyunca (veya daha uzun süre). Standart çözüm ilk girişte grubu atayın ve seçimi bir çerezde sürdürün:

// middleware.ts - A/B testing per homepage

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

const AB_COOKIE = 'ab_homepage';
const AB_VARIANTS = ['control', 'variant_a', 'variant_b'] as const;
type AbVariant = typeof AB_VARIANTS[number];

// Distribuzione del traffico: 50% control, 25% variant_a, 25% variant_b
const WEIGHTS = [0.5, 0.25, 0.25];

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

  // Applica A/B test solo sulla homepage
  if (pathname !== '/') {
    return NextResponse.next();
  }

  const response = NextResponse.next();

  // Controlla se l'utente ha gia un gruppo assegnato
  let variant = request.cookies.get(AB_COOKIE)?.value as AbVariant | undefined;

  if (!variant || !AB_VARIANTS.includes(variant as AbVariant)) {
    // Assegna un nuovo gruppo deterministicamente
    variant = assignVariant(request);

    // Salva il gruppo nel cookie (1 mese di durata)
    response.cookies.set(AB_COOKIE, variant, {
      maxAge: 30 * 24 * 60 * 60, // 30 giorni
      path: '/',
      httpOnly: false, // Visibile a JavaScript per il tracking
      sameSite: 'lax',
    });
  }

  // Aggiunge il gruppo come header per il server component e analytics
  response.headers.set('X-AB-Variant', variant);

  // Rewrite verso la variante corretta
  if (variant !== 'control') {
    const url = request.nextUrl.clone();
    url.pathname = `/experiments/homepage/${variant}`;
    return NextResponse.rewrite(url, { headers: response.headers });
  }

  return response;
}

function assignVariant(request: NextRequest): AbVariant {
  // Usa l'IP + User-Agent per un'assegnazione deterministica (non cambia tra ricariche)
  const seed = `${request.ip ?? ''}${request.headers.get('User-Agent') ?? ''}`;
  const hash = simpleHash(seed);
  const normalized = (hash % 1000) / 1000; // Normalizza a [0, 1)

  let cumulative = 0;
  for (let i = 0; i < WEIGHTS.length; i++) {
    cumulative += WEIGHTS[i];
    if (normalized < cumulative) return AB_VARIANTS[i];
  }

  return AB_VARIANTS[0]; // Fallback al control
}

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0; // Converte a 32-bit integer
  }
  return Math.abs(hash);
}

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

Kenardaki Özellik Bayrakları

Özellik bayrakları, alt kümeler için özellikleri etkinleştirmenize olanak tanır yeni bir dağıtıma gerek duymayan kullanıcıların sayısı. Edge ara yazılımıyla bayrakları okuyabilirsiniz bir edge yapılandırmasından (Vercel Edge Config) oluşturun ve bunları oluşturmadan önce uygulayın:

// middleware.ts - feature flags con Vercel Edge Config

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { get } from '@vercel/edge-config';

interface FeatureFlags {
  newDashboard: boolean;
  betaFeatures: string[]; // Lista di user IDs con accesso beta
  maintenanceMode: boolean;
}

export async function middleware(request: NextRequest): Promise<NextResponse> {
  // Edge Config ha latenza < 1ms (replicated globally)
  // get() e sincrono in termini di latenza percepita
  const flags = await get<FeatureFlags>('featureFlags');

  // Modalita manutenzione globale
  if (flags?.maintenanceMode) {
    const url = request.nextUrl.clone();
    url.pathname = '/maintenance';
    return NextResponse.rewrite(url);
  }

  // Reindirizza alla nuova dashboard se il flag e attivo
  if (flags?.newDashboard && request.nextUrl.pathname === '/dashboard') {
    const url = request.nextUrl.clone();
    url.pathname = '/dashboard-v2';
    return NextResponse.rewrite(url);
  }

  // Accesso beta: controlla se l'utente e nella lista
  const userId = request.cookies.get('user_id')?.value;
  if (userId && flags?.betaFeatures?.includes(userId)) {
    const response = NextResponse.next();
    response.headers.set('X-Beta-User', 'true');
    return response;
  }

  return NextResponse.next();
}

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

Ara Yazılımda Hafif Kimlik Doğrulaması

Middleware, JWT'yi doğrulamak ve yönlendirmek için harika bir yerdir İstek sunucuya ulaşmadan önce kimliği doğrulanmamış kullanıcılar:

// middleware.ts - verifica JWT leggera all'edge

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

const PUBLIC_PATHS = ['/login', '/register', '/api/auth', '/_next', '/favicon.ico'];
const AUTH_COOKIE = 'session_token';

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

  // Salta le route pubbliche
  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

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

  if (!token) {
    return redirectToLogin(request);
  }

  try {
    // Verifica il JWT usando Web Crypto API (disponibile nell'edge runtime)
    const isValid = await verifyJwt(token, request.headers.get('x-jwt-secret') ?? '');

    if (!isValid) {
      return redirectToLogin(request);
    }

    return NextResponse.next();
  } catch {
    return redirectToLogin(request);
  }
}

async function verifyJwt(token: string, secret: string): Promise<boolean> {
  try {
    const [headerB64, payloadB64, signatureB64] = token.split('.');
    if (!headerB64 || !payloadB64 || !signatureB64) return false;

    // Verifica la firma con HMAC-SHA256
    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      encoder.encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify']
    );

    const signature = base64UrlDecode(signatureB64);
    const data = encoder.encode(`${headerB64}.${payloadB64}`);

    const isValid = await crypto.subtle.verify('HMAC', key, signature, data);
    if (!isValid) return false;

    // Verifica la scadenza
    const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
    if (payload.exp && payload.exp < Date.now() / 1000) return false;

    return true;
  } catch {
    return false;
  }
}

function base64UrlDecode(str: string): ArrayBuffer {
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64 + '=='.slice(0, (4 - base64.length % 4) % 4);
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

function redirectToLogin(request: NextRequest): NextResponse {
  const url = request.nextUrl.clone();
  url.pathname = '/login';
  url.searchParams.set('callbackUrl', request.nextUrl.pathname);
  return NextResponse.redirect(url);
}

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

Vercel Edge Çalışma Zamanının Sınırlamaları

Vercel Edge Runtime, Node.js API'sinin bir alt kümesiyle V8'i temel alır. Dikkat edilmesi gereken ana sınırlamalar:

karakteristik Kenar Çalışma Zamanı Node.js Çalışma Zamanı
Soğuk başlangıç < 5ms ~50-500ms
Maksimum zaman aşımı 30'lar (Vercel Pro'da) 15 dakika (Vercel Enterprise'da)
Maksimum bellek 128MB 3 GB
fs modülü (dosya sistemi) Müsait değil Mevcut
Node.js yerleşikleri (yol, kripto düğümü) Kısmi Tamamlamak
Web Kripto API'si Mevcut Mevcut (Düğüm 17'den itibaren)
API'yi getir Yerli Yerel (Düğüm 18'den itibaren)
yerel bağlamalara sahip npm paketleri Desteklenmiyor Destekleniyor

Edge Runtime ile Uyumlu Olmayan Kitaplıklar

Birçok popüler npm kitaplığı Vercel Edge Runtime ile uyumlu değildir: Prisma (yerel sürücüleri kullanır), bcrypt (yerel bağlama), keskin (yerel görüntüler). Bir kitaplığı ara yazılımda kullanmadan önce, bunun uç çalışma zamanını desteklediğini doğrulayın. Prisma'nın kenar uyumlu bir adaptörü vardır (@prisma/adapter-pg ile @neondatabase/serverless), ancak tüm özellikler mevcut değildir.

Ara Yazılım Hata Ayıklama ve Test Etme

Ara katman yazılımı yerel olarak test edilebilir next dev. Bazı sınırlamalar: request.geo her zaman öyle undefined yerel olarak (bir sahte kullanın) ve Edge Config, ortam değişkenini gerektirir EDGE_CONFIG:

// middleware.ts con mock per development locale

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

function getGeo(request: NextRequest): { country?: string; region?: string } {
  // In development, simula la geolocalizzazione tramite header custom
  // Utile per testare diverse regioni senza deploy
  if (process.env.NODE_ENV === 'development') {
    return {
      country: request.headers.get('X-Mock-Country') ?? 'US',
      region: request.headers.get('X-Mock-Region') ?? 'CA',
    };
  }

  return {
    country: request.geo?.country,
    region: request.geo?.region,
  };
}

export function middleware(request: NextRequest): NextResponse {
  const { country, region } = getGeo(request);

  // Logica di routing geo-based
  const response = NextResponse.next();
  response.headers.set('X-Country', country ?? 'unknown');
  response.headers.set('X-Region', region ?? 'unknown');

  return response;
}

Sonuçlar ve Sonraki Adımlar

Vercel ara yazılımı en güçlü araçlardan biridir. Next.js uygulamasının davranışını değişiklik yapmadan özelleştirme sunucu tarafı mantığı. Oluşturmadan önce kenardaki konumu ve önbellek — hızlı yönlendirme kararları için idealdir veritabanı erişimi gerektirmezler.

Gösterilen modeller (coğrafi konum belirleme, A/B testi, özellik işaretleri, kimlik doğrulama) yaygın kullanım durumlarının çoğunu kapsarlar. Başarının anahtarı ara yazılımı hafif tutun: karmaşık mantık veya yavaş veritabanı erişimleri Sunucu Bileşenlerine veya Yönlendiricilere aittir.

Serideki Sonraki Yazılar

  • Madde 7: Sınırda Coğrafi Yönlendirme - Kişiselleştirme İçerik ve GDPR Uyumluluğu: Cloudflare Çalışanlarıyla coğrafi sınırlama, yerelleştirilmiş fiyatlandırma ve yerel düzenlemeler nedeniyle engelleyin.
  • Madde 8: Cloudflare'de Önbellek API'si ve Geçersiz Kılma Stratejileri İşçiler: Üretim sırasında eskimiş yeniden doğrulama ve geçersiz kılma.
  • Madde 9: Yerelde İşçilerin Test Edilmesi — Miniflare, Vitest ve Wrangler Dev: dağıtıma gerek kalmadan test için eksiksiz iş akışı.