Vercel Edge Middleware: Los 4 Patrones que el 90% Implementa Mal
Guía práctica de Vercel Edge Middleware: autenticación stateless, A/B testing, geo-routing y request transformation. Patrones avanzados para desplegar en el edge.
El 90% de los Desarrolladores Usa Vercel Edge Middleware Como Proxy, No Como Motor de Lógica
Vercel Edge Middleware lleva años disponible. Pocos lo aprovechan más allá de redirecciones básicas.
La mayoría implementa Edge Middleware como un simple filtro de requests. Redirecciones HTTP. Headers básicos. Nada más.
*El potencial real está en transformar Edge Middleware en un motor de lógica de negocio que ejecuta antes de que tu aplicación siquiera reciba la petición.*
Autenticación sin cookies de sesión. A/B testing sin JavaScript del cliente. Geo-routing con latencia cero. Transformación de requests que elimina carga de tus Serverless Functions.
Esto no es teoría. Son patrones que el 10% de los proyectos en Vercel implementa correctamente.
El Problema: Edge Middleware No Es Un Middleware Tradicional
Si vienes de Express o Django, Edge Middleware de Vercel te va a sorprender.
No es un sistema de plugins encadenados. No hay `next()`. No hay `res.next()`.
Vercel Edge Middleware recibe un objeto `Request` y devuelve un objeto `Response`. Punto. Puedes inspectar, transformar, y decidir qué hacer con cada request antes de que llegue a tu aplicación.
```typescript
// ❌ LO QUE EL 90% HACE — Redirección básica
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/new-path', request.url));
}
```
Este patrón funciona. Pero desperdicia el potencial completo del edge.
```typescript
// ✅ LO QUE EL 10% HACE — Lógica de negocio en el edge
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
// Validación de JWT en el edge — sin tocar Serverless Functions
const isValid = await validateToken(token);
if (!isValid && !isPublicRoute(request.nextUrl.pathname)) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Inyección de headers para tracking
const response = NextResponse.next();
response.headers.set('x-user-region', getRegion(request));
return response;
}
```
La diferencia: el primer ejemplo apenas usa el edge. El segundo ejecuta lógica de negocio donde la latencia es mínima.
Patrón 1: Autenticación Stateless en el Edge
El error más común en autenticación con Next.js es delegar la validación de tokens a Serverless Functions.
Cada request que requiere autenticación pasa por:
1. Edge Middleware → redirige a Serverless Function
2. Serverless Function → valida token → responde
Esto añade latencia innecesaria. Especialmente si tu Serverless Function está desplegada en una región diferente.
*La solución: validar tokens JWT directamente en Edge Middleware usando una librería compatible con Edge Runtime.*
```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose'; // Compatible con Edge Runtime
// Secret en variable de entorno (Vercel lo cifra automáticamente)
const secret = new TextEncoder().encode(
process.env.JWT_SECRET
);
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
const pathname = request.nextUrl.pathname;
// Rutas públicas que no requieren auth
const publicRoutes = ['/login', '/register', '/forgot-password'];
const isPublic = publicRoutes.some(route => pathname.startsWith(route));
if (isPublic) {
return NextResponse.next();
}
// Si no hay token y la ruta es protegida
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
try {
// Validación en el edge — no hay cold start, no hay region penalty
const { payload } = await jwtVerify(token, secret);
// Inyectar user info en headers para que Serverless Functions lo lean
const response = NextResponse.next();
response.headers.set('x-user-id', payload.sub as string);
response.headers.set('x-user-role', payload.role as string);
return response;
} catch {
// Token expirado o inválido
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('session');
return response;
}
}
export const config = {
matcher: [
/*
Coincide con todas las rutas excepto:
- /api (las APIs manejan su propia auth)
- /_next (archivos internos de Next.js)
- /static (archivos estáticos)
- /favicon.ico
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
```
El resultado: validación de autenticación en menos de 5msg. Tu Serverless Function recibe headers con el usuario ya validado.
Patrón 2: A/B Testing Sin JavaScript del Cliente
El A/B testing tradicional requiere cargar una librería JavaScript en el cliente. Optimizely, Google Optimize, VWO. Todos añaden peso a tu bundle.
Con Edge Middleware, el split de variantes ocurre en el servidor, antes de que el HTML llegue al navegador.
```typescript
// middleware.ts — A/B Testing en el edge
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { sha256 } from 'crypto';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Solo aplicar A/B en rutas específicas
if (!['/', '/pricing', '/features'].includes(pathname)) {
return NextResponse.next();
}
// 1. Mirar si el usuario ya tiene variante asignada
let variant = request.cookies.get('ab_variant')?.value;
if (!variant) {
// 2. Generar variante basada en hash de cookie de sesión
// Esto asegura que el mismo usuario siempre ve la misma variante
const sessionId = request.cookies.get('_ga')?.value ||
request.headers.get('x-forwarded-for') ||
crypto.randomUUID();
const hash = parseInt(sha256(sessionId).substring(0, 8), 16);
variant = hash % 100 < 50 ? 'control' : 'variant_b';
// Guardar en cookie para consistencia
const response = NextResponse.next();
response.cookies.set('ab_variant', variant, {
maxAge: 60 60 24 * 30, // 30 días
path: '/',
sameSite: 'lax',
});
// Pasar variante a tu aplicación vía header
response.headers.set('x-ab-variant', variant);
return response;
}
// 3. Usuario existente — pasar variante a headers
const response = NextResponse.next();
response.headers.set('x-ab-variant', variant);
return response;
}
```
En tu Server Component o API Route, lees el header directamente:
```typescript
// app/page.tsx — Leyendo variante en Server Component
import { headers } from 'next/headers';
export default async function HomePage() {
const headersList = await headers();
const variant = headersList.get('x-ab-variant') || 'control';
return (
<div>
{variant === 'variant_b' ? (
<Hero variant="aggressive" />
) : (
<Hero variant="conservative" />
)}
</div>
);
}
```
Resultado: A/B testing sin JavaScript adicional en el cliente. Time to First Byte reducido. Tracking completamente server-side.
Patrón 3: Geo-Routing con Datos de geolocalización
Vercel proporciona headers de geolocalización automáticamente en Edge Middleware. No necesitas un servicio externo de geolocation.
```typescript
// middleware.ts — Geo-Routing
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const country = request.geo?.country;
const city = request.geo?.city;
const region = request.geo?.region;
const pathname = request.nextUrl.pathname;
// Redirigir based en país
if (pathname === '/pricing') {
if (country === 'ES') {
// España: precios en euros, IVA incluido
const url = request.nextUrl.clone();
url.searchParams.set('currency', 'EUR');
url.searchParams.set('region', 'EU');
return NextResponse.rewrite(url);
}
if (country === 'US') {
// Estados Unidos: precios en dólares
const url = request.nextUrl.clone();
url.searchParams.set('currency', 'USD');
url.searchParams.set('region', 'NA');
return NextResponse.rewrite(url);
}
// Resto del mundo: precios en dólares, sin IVA
const url = request.nextUrl.clone();
url.searchParams.set('currency', 'USD');
url.searchParams.set('region', 'ROW');
return NextResponse.rewrite(url);
}
// Rewrite inteligente para contenido localizado
if (pathname === '/docs') {
const supportedLocales = ['es', 'en', 'fr', 'de'];
const locale = determineLocale(request, supportedLocales);
const url = request.nextUrl.clone();
url.pathname = `/docs/${locale}/index`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
function determineLocale(request: NextRequest, supported: string[]): string {
// 1. Mirar cookie de preferencia
const cookieLocale = request.cookies.get('preferred_locale')?.value;
if (cookieLocale && supported.includes(cookieLocale)) {
return cookieLocale;
}
// 2. Mirar header Accept-Language
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferred = acceptLanguage.split(',')[0].split('-')[0];
if (supported.includes(preferred)) {
return preferred;
}
}
// 3. Fallback a geo-based
const country = request.geo?.country;
const countryToLocale: Record<string, string> = {
'ES': 'es',
'MX': 'es',
'AR': 'es',
'CO': 'es',
'GB': 'en',
'US': 'en',
'FR': 'fr',
'DE': 'de',
};
return countryToLocale[country || ''] || 'en';
}
```
*La diferencia entre rewrite y redirect importa aquí.* `NextResponse.rewrite()` mantiene la URL visible para el usuario mientras carga contenido diferente. `NextResponse.redirect()` cambia la URL.
Patrón 4: Transformación de Requests
El edge es el lugar perfecto para normalizar, sanitizar, o transformar requests antes de que lleguen a tu aplicación.
Casos de uso reales:
Normalizar headers inconsistentes de diferentes clientes
Añadir información de contexto (user agent parsing, device detection)
Logging centralizado sin tocar tu lógica de negocio
Rate limiting básico antes de que el request entre en tu infraestructura
```typescript
// middleware.ts — Request Transformation
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { parseUserAgent } from '@vercel/edge';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 1. Normalizar headers de origen
const forwardedFor = request.headers.get('x-forwarded-for');
const realIp = request.headers.get('x-real-ip');
response.headers.set('x-client-ip', forwardedFor?.split(',')[0] || realIp || 'unknown');
// 2. Detección de dispositivo desde User-Agent
const userAgent = request.headers.get('user-agent') || '';
const deviceInfo = parseUserAgent(userAgent);
response.headers.set('x-device-type', deviceInfo.device?.type || 'desktop');
response.headers.set('x-os', deviceInfo.os?.name || 'unknown');
response.headers.set('x-browser', deviceInfo.browser?.name || 'unknown');
// 3. Añadir timestamp de procesamiento en edge
response.headers.set('x-edge-time', new Date().toISOString());
// 4. Headers de debug en entorno de desarrollo
if (process.env.NODE_ENV === 'development') {
response.headers.set('x-debug-edge', 'true');
response.headers.set('x-request-id', crypto.randomUUID());
}
// 5. CSP headers dinámicos basados en ruta
const pathname = request.nextUrl.pathname;
if (pathname.startsWith('/api/')) {
// APIs más permisivas (graphQL, REST endpoints)
response.headers.set(
'Content-Security-Policy',
"default-src 'none'; script-src 'self'"
);
} else {
// Páginas web: política más estricta
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
}
return response;
}
```
El Patrón de Middleware Modular en 4 Capas
Después de implementar docenas de proyectos en Vercel, he identificado un framework que escala sin volverse inmanejable.
El Patrón de Middleware Modular en 4 Capas estructura Edge Middleware en capas separadas, cada una con responsabilidad única:
Capa 1: Context Setup
Primera función que ejecuta. Extrae información básica del request. No toma decisiones.
```typescript
// layers/01-context.ts
import type { NextRequest } from 'next/server';
export interface RequestContext {
ip: string;
device: string;
locale: string;
userId: string | null;
}
export function extractContext(request: NextRequest): RequestContext {
return {
ip: request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown',
device: request.headers.get('x-device-type') || 'desktop',
locale: request.cookies.get('locale')?.value || 'es',
userId: request.cookies.get('user_id')?.value || null,
};
}
```
Capa 2: Feature Flags
Evalúa flags de funcionalidad. Decide qué features están activas para este request.
```typescript
// layers/02-features.ts
import type { NextRequest } from 'next/server';
import type { RequestContext } from './01-context';
export interface FeatureFlags {
newCheckout: boolean;
betaDashboard: boolean;
aiAssistant: boolean;
}
export function evaluateFeatures(
request: NextRequest,
context: RequestContext
): FeatureFlags {
const isPremium = request.cookies.get('plan')?.value === 'premium';
return {
newCheckout: context.locale === 'es', // Solo España por ahora
betaDashboard: isPremium,
aiAssistant: context.userId !== null,
};
}
```
Capa 3: Access Control
Evaluación de permisos basada en contexto y features. Decide si el request proceede o se rechaza.
```typescript
// layers/03-access.ts
import type { NextRequest } from 'next/server';
import type { RequestContext } from './01-context';
import type { FeatureFlags } from './02-features';
export type AccessDecision = 'allow' | 'redirect' | 'block';
export function evaluateAccess(
request: NextRequest,
context: RequestContext,
features: FeatureFlags
): AccessDecision {
const pathname = request.nextUrl.pathname;
// Rutas protegidas
if (pathname.startsWith('/dashboard') && !context.userId) {
return 'redirect';
}
// Feature gating
if (pathname.startsWith('/beta') && !features.betaDashboard) {
return 'block';
}
return 'allow';
}
```
Capa 4: Response Enrichment
Añade headers, cookies, y metadata al response. Esta capa siempre ejecuta, incluso si access fue denegado.
```typescript
// layers/04-enrichment.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import type { RequestContext } from './01-context';
import type { FeatureFlags } from './02-features';
import type { AccessDecision } from './03-access';
export function enrichResponse(
request: NextRequest,
response: NextResponse,
context: RequestContext,
features: FeatureFlags,
access: AccessDecision
): NextResponse {
// Siempre añadir context headers
response.headers.set('x-edge-user-id', context.userId || 'anonymous');
response.headers.set('x-edge-locale', context.locale);
// Feature flags en headers para debugging
response.headers.set('x-features', JSON.stringify(features));
// Headers de seguridad
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
// Headers de analytics (no interfieren con caching)
response.headers.set('x-anon-id', context.ip);
return response;
}
```
Middleware principal que orchestra las capas:
```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { extractContext } from './layers/01-context';
import { evaluateFeatures } from './layers/02-features';
import { evaluateAccess, type AccessDecision } from './layers/03-access';
import { enrichResponse } from './layers/04-enrichment';
export async function middleware(request: NextRequest) {
// 1. Extraer contexto
const context = extractContext(request);
// 2. Evaluar features
const features = evaluateFeatures(request, context);
// 3. Evaluar acceso
const access = evaluateAccess(request, context, features);
// 4. Preparar response base
let response: NextResponse;
switch (access) {
case 'redirect':
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
response = NextResponse.redirect(loginUrl);
break;
case 'block':
response = NextResponse.json(
{ error: 'Access denied' },
{ status: 403 }
);
break;
case 'allow':
default:
response = NextResponse.next();
}
// 5. Enrich con headers y metadata
return enrichResponse(request, response, context, features, access);
}
```
Limitaciones Conocidas de Edge Middleware
Edge Middleware tiene constraints que debes conocer antes de adoptarlo masivamente.
No disponible:
Acceso a filesystem
child_process para ejecutar binaries
Conexiones a databases via TCP directa (usa conectores de Edge)
Crypto avanzado más allá de SubtleCrypto API
Logging a servicios externos (no hay fetch a APIs sin límite de tiempo)
Disponible pero con cuidado:
Fetch a APIs externas: funciona, pero el timeout es agresivo (~50ms para cold start)
Cookies: lectura y escritura OK, pero límites de tamaño apply
Headers: ciertos headers no pueden ser modificados ( Content-Length, Transfer-Encoding)
Si necesitas lógica que excede estas limitaciones, usa Serverless Functions normales como fallback.
Conclusión
Edge Middleware no es un proxy. Es un motor de lógica de negocio con latencia mínima.
Los 4 patrones que hemos cubierto — autenticación stateless, A/B testing server-side, geo-routing, y request transformation — son el mínimo viable para cualquier proyecto serio en Vercel.
El Patrón de Middleware Modular en 4 Capas te permite escalar esta lógica sin que tu middleware se convierta en un archivo spaghetti de 500 líneas.
La próxima vez que escribas `if (pathname === '/') return NextResponse.next()`, pregúntate qué otra cosa podrías estar haciendo en ese momento. Probablemente, mucho.
El edge está esperando a que lo aproveches.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

