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.
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