Salta al contenuto principale

06 - Vercel Edge Runtime: Middleware Avanzato, Geolocalizzazione e A/B Testing

Edge Computing 13 min Vercel Edge Runtime: Middleware Avanzato, Geolocalizzazione e A/B Testing Con Vercel, il middleware edge esegue prima del render SSR...

Cosa ne pensi di questo articolo?

06 - Vercel Edge Runtime: Middleware Avanzato, Geolocalizzazione e A/B Testing
Edge ComputingFoto:Daniil KomovPexels

In Breve

Edge Computing 13 min Vercel Edge Runtime: Middleware Avanzato, Geolocalizzazione e A/B Testing Con Vercel, il middleware edge esegue prima del render SSR...

Edge Computing 13 min

Vercel Edge Runtime: Middleware Avanzato, Geolocalizzazione e A/B Testing

Con Vercel, il middleware edge esegue prima del render SSR permettendo routing basato su geolocalizzazione, feature flags, A/B testing e autenticazione senza cold start, tutto in meno di 50ms in media globale.

Vercel Edge Next.js Middleware Geolocation A/B Testing Edge Runtime

Middleware Prima del Rendering

In un'applicazione Next.js tradizionale deployata su Vercel, ogni richiesta raggiunge prima il CDN, poi - se non in cache - viene inoltrata al server SSR. Il middleware edge si inserisce tra questi due stadi: viene eseguito al bordo della rete Vercel, prima del routing verso il server SSR e prima che la risposta venga servita dalla cache.

Questo posizionamento è fondamentale: il middleware può modificare la richiesta, redirigere l'utente, impostare cookie o header, e tutto ciò accade in un runtime V8-based con cold start vicino a zero. Vercel misura una latenza di P50 < 10ms per la maggior parte dei middleware nelle loro regioni edge (100+ globali).

Cosa Imparerai

  • Struttura e file convention del middleware in Next.js App Router
  • Geolocalizzazione: routing per paese, regione e lingua
  • A/B testing stabile con cookie persistenti
  • Feature flags all'edge senza round-trip al server
  • Autenticazione leggera nel middleware
  • Limiti del Vercel Edge Runtime rispetto a Node.js
  • Debug locale del middleware con next dev

Il File middleware.ts

In Next.js (App Router e Pages Router), il middleware è definito nel file middleware.ts (o .js) nella root del progetto. La funzione esportata riceve un oggetto NextRequest e deve restituire un 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)$).*)',
  ],
};

Geolocalizzazione: Routing per Paese e Lingua

Vercel arricchisce ogni richiesta con informazioni geografiche tramite l'oggetto request.geo. In production (non in local dev), sono disponibili paese, regione e città:

// 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+).*)'],
};

A/B Testing Stabile con Cookie

L'A/B testing al edge richiede che ogni utente rimanga nel proprio gruppo per tutta la sessione (o più a lungo). La soluzione standard è assegnare il gruppo al primo accesso e persistere la scelta in un cookie:

// 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: ['/'],
};

Feature Flags all'Edge

I feature flags permettono di abilitare funzionalità per sottoinsiemi di utenti senza un nuovo deploy. Con il middleware edge, puoi leggere i flag da un edge config (Vercel Edge Config) e applicarli prima del rendering:

// 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+).*)'],
};

Autenticazione Leggera nel Middleware

Il middleware è il posto ideale per verificare JWT e redirigere gli utenti non autenticati prima che la richiesta raggiunga il server:

// 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+).*)'],
};

Limiti del Vercel Edge Runtime

Il Vercel Edge Runtime è basato su V8 con un sottoinsieme delle API Node.js. I limiti principali da conoscere:

Caratteristica Edge Runtime Node.js Runtime
Cold start < 5ms ~50-500ms
Timeout massimo 30s (su Vercel Pro) 15min (su Vercel Enterprise)
Memoria massima 128MB 3GB
fs module (filesystem) Non disponibile Disponibile
Node.js built-ins (path, crypto node) Parziale Completo
Web Crypto API Disponibile Disponibile (da Node 17)
Fetch API Nativa Nativa (da Node 18)
npm packages con native bindings Non supportati Supportati

Librerie Incompatibili con Edge Runtime

Molte librerie npm popolari non sono compatibili con il Vercel Edge Runtime: Prisma (usa driver native), bcrypt (native binding), sharp (immagini native). Prima di usare una libreria nel middleware, verifica che supporti l'edge runtime. Prisma ha un adapter edge-compatible (@prisma/adapter-pg con @neondatabase/serverless), ma non tutte le feature sono disponibili.

Debug e Test del Middleware

Il middleware è testabile localmente con next dev. Alcune limitazioni: request.geo è sempre undefined in locale (usa un mock), e Edge Config richiede la variabile d'ambiente 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;
}

Conclusioni e Prossimi Passi

Il middleware Vercel è uno degli strumenti più potenti per personalizzare il comportamento di un'applicazione Next.js senza modificare la logica server-side. La sua posizione all'edge - prima del rendering e della cache - lo rende ideale per decisioni di routing rapide che non richiedono accesso al database.

I pattern mostrati (geolocalizzazione, A/B testing, feature flags, autenticazione) coprono la maggior parte dei casi d'uso comuni. La chiave del successo è mantenere il middleware leggero: logica complessa o accessi a database lenti appartengono ai Server Components o alle Route Handlers.

Prossimi Articoli della Serie

  • Articolo 7: Routing Geografico all'Edge - Personalizzazione Contenuti e Compliance GDPR: geo-fencing con Cloudflare Workers, prezzi localizzati e blocco per regolamentazioni locali.
  • Articolo 8: Cache API e Strategie di Invalidation in Cloudflare Workers: stale-while-revalidate e invalidation per chiave in produzione.
  • Articolo 9: Testing di Workers in Local - Miniflare, Vitest e Wrangler Dev: workflow completo per testare senza deploy.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti