Vercel Edge Runtime: geavanceerde middleware, geolocatie en A/B-testen
Met Vercel voert de edge-middleware eerst SSR-rendering uit, waardoor routering mogelijk wordt gebaseerd op geolocatie, functievlaggen, A/B-testen en authenticatie zonder koude start, alles in minder dan 50 ms gemiddeld wereldwijd.
Middleware vóór weergave
In een traditionele Next.js-applicatie die op Vercel wordt ingezet, wordt elk verzoek het bereikt eerst het CDN en wordt vervolgens – als het niet in de cache zit – doorgestuurd naar de SSR-server. Edge-middleware past tussen deze twee fasen: komt uitgevoerd aan de rand van het Vercel-netwerk, voordat het naar de SSR-server wordt geleid en voordat het antwoord vanuit de cache wordt geserveerd.
Deze positionering is van cruciaal belang: de middleware kan worden gewijzigd het verzoek indienen, de gebruiker omleiden, cookies of headers instellen, en zo gebeurt in een op V8 gebaseerde runtime met een koude start nabij nul. Vercel maatregelen een latentie van P50 < 10 ms voor de meeste middleware in hun randregio's (100+ wereldwijd).
Wat je gaat leren
- Middleware-structuur en bestandsconventie in Next.js App Router
- Geolocatie: routebepaling per land, regio en taal
- Stabiele A/B-testen met permanente cookies
- Voorzien van vlaggen aan de rand zonder retourvluchten naar de server
- Lichtgewicht authenticatie in middleware
- Beperkingen van de Vercel Edge Runtime vergeleken met Node.js
- Lokale middleware-foutopsporing met
next dev
Het bestand middleware.ts
In Next.js (App Router en Pages Router) wordt de middleware in het bestand gedefinieerd
middleware.ts (o .js) in de projectroot.
De geëxporteerde functie ontvangt een object NextRequest en moet teruggeven
een 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)$).*)',
],
};
Geolocatie: routering op land en taal
Vercel verrijkt elke aanvraag met geografische informatie via het onderwerp
request.geo. In productie (niet in lokale ontwikkeling) zijn ze beschikbaar
land, regio en stad:
// 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+).*)'],
};
Stabiele A/B-testen met cookies
Bij A/B-testen aan de rand moet elke gebruiker in zijn eigen groep blijven gedurende de sessie (of langer). De standaardoplossing is wijs de groep toe bij de eerste login en zet de keuze vast in een 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: ['/'],
};
Functievlaggen aan de rand
Met functievlaggen kunt u functies voor subsets inschakelen van gebruikers zonder een nieuwe implementatie. Met edge-middleware kunt u vlaggen lezen vanuit een edge-configuratie (Vercel Edge Config) en pas deze toe voordat u rendert:
// 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+).*)'],
};
Lichtgewicht authenticatie in middleware
Middleware is een geweldige plek om JWT te verifiëren en om te leiden niet-geverifieerde gebruikers voordat het verzoek de server bereikt:
// 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+).*)'],
};
Beperkingen van de Vercel Edge Runtime
De Vercel Edge Runtime is gebaseerd op V8 met een subset van de Node.js API. De belangrijkste beperkingen waar u rekening mee moet houden:
| Kenmerkend | Edge-runtime | Node.js-runtime |
|---|---|---|
| Koud begin | < 5 ms | ~50-500 ms |
| Maximale time-out | Jaren '30 (op Vercel Pro) | 15min (op Vercel Enterprise) |
| Maximaal geheugen | 128 MB | 3 GB |
| fs-module (bestandssysteem) | Niet beschikbaar | Beschikbaar |
| Ingebouwde Node.js (pad, cryptoknooppunt) | Gedeeltelijk | Compleet |
| Webcrypto-API | Beschikbaar | Beschikbaar (vanaf knooppunt 17) |
| API ophalen | Oorspronkelijk | Native (vanaf knooppunt 18) |
| npm-pakketten met native bindingen | Niet ondersteund | Ondersteund |
Bibliotheken zijn niet compatibel met Edge Runtime
Veel populaire npm-bibliotheken zijn niet compatibel met de Vercel Edge Runtime:
Prisma (gebruikt native stuurprogramma's), bcrypt (native binding), scherp (native afbeeldingen).
Voordat u een bibliotheek in middleware gebruikt, moet u controleren of deze de edge-runtime ondersteunt.
Prisma heeft een edge-compatibele adapter (@prisma/adapter-pg met
@neondatabase/serverless), maar niet alle functies zijn beschikbaar.
Debuggen en testen van middleware
De middleware kan lokaal worden getest met next dev.
Enkele beperkingen: request.geo dat is altijd zo undefined
lokaal (gebruik een nepversie) en Edge Config vereist de omgevingsvariabele
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;
}
Conclusies en volgende stappen
Vercel middleware is een van de krachtigste tools voor pas het gedrag van een Next.js-toepassing zonder wijziging aan logica aan de serverzijde. De positie aan de rand — vóór weergave en cache — maakt het ideaal voor snelle routeringsbeslissingen ze vereisen geen databasetoegang.
De getoonde patronen (geolokalisatie, A/B-testen, functievlaggen, authenticatie) ze dekken de meeste veelvoorkomende gebruiksscenario's. De sleutel tot succes is houd middleware lichtgewicht: complexe logica of trage databasetoegang behoren tot servercomponenten of routehandlers.
Volgende artikelen in de serie
- Artikel 7: Geografische routing aan de rand: personalisatie Inhoud en AVG-naleving: geofencing met Cloudflare Workers, gelokaliseerde prijzen en blokkeren vanwege lokale regelgeving.
- Artikel 8: Cache-API en invalidatiestrategieën in Cloudflare Werknemers: verouderd-terwijl-revalideren en ongeldig maken door middel van een sleutel in de productie.
- Artikel 9: Testen van werknemers op lokaal niveau — Miniflare, Vitest en Wrangler Dev: complete workflow voor testen zonder implementatie.







