DevToolBoxGRATIS
Blog

Next.js Middleware: Autenticación, Redirecciones y Rate Limiting

14 minpor DevToolBox

El middleware de Next.js se ejecuta antes de que una solicitud se complete, en el Edge Runtime, ideal para autenticación, redirecciones geográficas, pruebas A/B y limitación de velocidad.

¿Qué es el Middleware de Next.js?

El middleware es una función que se ejecuta antes de que la solicitud llegue a tu página.

// Request flow in Next.js
//
// Client Request
//     │
//     ▼
// ┌─────────────┐
// │  Middleware  │  ← Runs here (Edge Runtime)
// └─────────────┘
//     │
//     ▼
// ┌─────────────┐
// │  Page/API    │  ← Your application code
// └─────────────┘
//     │
//     ▼
// Client Response

Configuración básica

Crea un archivo middleware.ts en la raíz del proyecto.

// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Log every request
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);

  // Continue to the next handler
  return NextResponse.next();
}

// Only run on specific paths
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Configuración del Matcher

El matcher determina qué rutas activan el middleware.

// Match specific routes
export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
};

// Match all routes except static files and API
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

// Multiple matchers with different patterns
export const config = {
  matcher: [
    '/dashboard/:path*',     // All dashboard routes
    '/admin/:path*',         // All admin routes
    '/api/protected/:path*', // Protected API routes
    '/profile',              // Exact match
  ],
};

// Conditional matching with has/missing (Next.js 13.1+)
export const config = {
  matcher: [
    {
      source: '/api/:path*',
      has: [
        { type: 'header', key: 'authorization' },
      ],
    },
  ],
};

Middleware de autenticación

Proteger rutas que requieren autenticación.

Verificación de JWT

Verificar tokens JWT directamente en el middleware.

// middleware.ts — JWT Authentication
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET!
);

const PUBLIC_PATHS = ['/login', '/register', '/forgot-password'];

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

  // Skip public paths
  if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // Get token from cookie or Authorization header
  const token =
    request.cookies.get('auth-token')?.value ||
    request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    // Redirect to login with return URL
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    // Verify JWT token
    const { payload } = await jwtVerify(token, JWT_SECRET);

    // Add user info to headers for downstream use
    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 (error) {
    // Token expired or invalid — redirect to login
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete('auth-token');
    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/api/protected/:path*'],
};

Control de acceso basado en roles

Control de acceso según roles de usuario.

// Role-based access control in middleware
const ROUTE_PERMISSIONS: Record<string, string[]> = {
  '/admin':        ['admin'],
  '/dashboard':    ['admin', 'editor', 'viewer'],
  '/editor':       ['admin', 'editor'],
  '/api/admin':    ['admin'],
};

function hasPermission(pathname: string, role: string): boolean {
  for (const [route, roles] of Object.entries(ROUTE_PERMISSIONS)) {
    if (pathname.startsWith(route)) {
      return roles.includes(role);
    }
  }
  return true; // No restriction for unmatched routes
}

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  if (!token) return NextResponse.redirect(new URL('/login', request.url));

  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);
    const role = payload.role as string;
    const { pathname } = request.nextUrl;

    if (!hasPermission(pathname, role)) {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }

    return NextResponse.next();
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

Patrones de redirección

Ideal para redirecciones condicionales.

Redirecciones geográficas

Redirigir usuarios según su geolocalización.

// Geo-based redirect (Vercel Edge)
export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';
  const city = request.geo?.city || 'Unknown';

  // Redirect EU users to GDPR-compliant page
  const EU_COUNTRIES = [
    'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI',
    'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',
    'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
  ];

  if (EU_COUNTRIES.includes(country) &&
      request.nextUrl.pathname === '/') {
    return NextResponse.redirect(new URL('/eu', request.url));
  }

  // Add geo headers for downstream use
  const response = NextResponse.next();
  response.headers.set('x-country', country);
  response.headers.set('x-city', city);
  return response;
}

Detección de idioma

Detectar idioma preferido y redirigir.

// Locale detection middleware
const SUPPORTED_LOCALES = ['en', 'fr', 'de', 'es', 'ja', 'ko', 'zh'];
const DEFAULT_LOCALE = 'en';

function getPreferredLocale(request: NextRequest): string {
  // Check cookie first (user preference)
  const cookieLocale = request.cookies.get('locale')?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
    return cookieLocale;
  }

  // Parse Accept-Language header
  const acceptLang = request.headers.get('accept-language') || '';
  const preferred = acceptLang
    .split(',')
    .map(lang => lang.split(';')[0].trim().substring(0, 2))
    .find(lang => SUPPORTED_LOCALES.includes(lang));

  return preferred || DEFAULT_LOCALE;
}

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

  // Check if locale is already in the path
  const hasLocale = SUPPORTED_LOCALES.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (hasLocale) return NextResponse.next();

  // Redirect to locale-prefixed path
  const locale = getPreferredLocale(request);
  const url = new URL(`/${locale}${pathname}`, request.url);
  return NextResponse.redirect(url);
}

Limitación de velocidad

Implementar limitación en el borde.

// Simple rate limiting with in-memory store
// Note: This works per-edge-node; use Redis for distributed rate limiting

const rateLimit = new Map<string, { count: number; resetTime: number }>();

const RATE_LIMIT = 100;       // requests
const RATE_WINDOW = 60 * 1000; // per minute

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const record = rateLimit.get(ip);

  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + RATE_WINDOW });
    return false;
  }

  record.count++;
  return record.count > RATE_LIMIT;
}

export function middleware(request: NextRequest) {
  // Only rate-limit API routes
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ||
             request.ip ||
             '127.0.0.1';

  if (isRateLimited(ip)) {
    return new NextResponse(
      JSON.stringify({ error: 'Too many requests' }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
          'Retry-After': '60',
        },
      }
    );
  }

  return NextResponse.next();
}

Ventana deslizante

Algoritmo de ventana deslizante.

// Rate limiting with Upstash Redis (production-ready)
// npm install @upstash/ratelimit @upstash/redis

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
  analytics: true,
  prefix: 'api-ratelimit',
});

export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const ip = request.ip ?? '127.0.0.1';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  const response = success
    ? NextResponse.next()
    : new NextResponse(JSON.stringify({ error: 'Rate limit exceeded' }), {
        status: 429,
        headers: { 'Content-Type': 'application/json' },
      });

  // Always set rate limit headers
  response.headers.set('X-RateLimit-Limit', limit.toString());
  response.headers.set('X-RateLimit-Remaining', remaining.toString());
  response.headers.set('X-RateLimit-Reset', reset.toString());

  return response;
}

Encabezados y seguridad

Lugar ideal para encabezados de seguridad.

Encabezados de seguridad

// Security headers middleware
export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Prevent XSS attacks
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  );

  // Prevent clickjacking
  response.headers.set('X-Frame-Options', 'DENY');

  // Prevent MIME type sniffing
  response.headers.set('X-Content-Type-Options', 'nosniff');

  // Enable HSTS
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=63072000; includeSubDomains; preload'
  );

  // Referrer policy
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions policy
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );

  return response;
}

Encabezados CORS

Manejar CORS a nivel de middleware.

// CORS middleware for API routes
const ALLOWED_ORIGINS = [
  'https://myapp.com',
  'https://staging.myapp.com',
  process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
].filter(Boolean);

export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  const origin = request.headers.get('origin') || '';
  const isAllowed = ALLOWED_ORIGINS.includes(origin);

  // Handle preflight OPTIONS request
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': isAllowed ? origin : '',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  const response = NextResponse.next();
  if (isAllowed) {
    response.headers.set('Access-Control-Allow-Origin', origin);
    response.headers.set('Access-Control-Allow-Credentials', 'true');
  }
  return response;
}

Pruebas A/B

Pruebas A/B del lado del servidor sin parpadeo.

// A/B Testing middleware
export function middleware(request: NextRequest) {
  // Only A/B test the homepage
  if (request.nextUrl.pathname !== '/') {
    return NextResponse.next();
  }

  // Check if user already has a variant assigned
  const variant = request.cookies.get('ab-variant')?.value;

  if (variant) {
    // Rewrite to the variant page without changing URL
    return NextResponse.rewrite(
      new URL(`/variants/${variant}`, request.url)
    );
  }

  // Assign a random variant (50/50 split)
  const newVariant = Math.random() < 0.5 ? 'control' : 'experiment';

  const response = NextResponse.rewrite(
    new URL(`/variants/${newVariant}`, request.url)
  );

  // Persist variant in cookie for 30 days
  response.cookies.set('ab-variant', newVariant, {
    maxAge: 60 * 60 * 24 * 30,
    httpOnly: true,
    sameSite: 'lax',
  });

  return response;
}

Reescritura de URL

Reescribir sin cambiar la URL del navegador.

// Multi-tenant rewriting
// app.mysite.com → /tenants/app
// docs.mysite.com → /tenants/docs
export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  const subdomain = hostname.split('.')[0];

  if (subdomain && subdomain !== 'www' && subdomain !== 'mysite') {
    return NextResponse.rewrite(
      new URL(`/tenants/${subdomain}${request.nextUrl.pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

// Proxy API requests to external service
// /api/external/* → https://api.external-service.com/*
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/external/')) {
    const path = request.nextUrl.pathname.replace('/api/external', '');
    return NextResponse.rewrite(
      new URL(`https://api.external-service.com${path}`)
    );
  }
  return NextResponse.next();
}

Encadenamiento de middleware

Componer múltiples funciones middleware.

// Composing multiple middleware functions
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

type MiddlewareFn = (
  request: NextRequest,
  response: NextResponse
) => NextResponse | Promise<NextResponse>;

function chain(
  functions: MiddlewareFn[],
  request: NextRequest
): Promise<NextResponse> {
  return functions.reduce(
    async (promise, fn) => {
      const response = await promise;
      return fn(request, response);
    },
    Promise.resolve(NextResponse.next())
  );
}

// Individual middleware functions
const withLogging: MiddlewareFn = (request, response) => {
  console.log(`[${request.method}] ${request.nextUrl.pathname}`);
  return response;
};

const withSecurityHeaders: MiddlewareFn = (request, response) => {
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  return response;
};

const withAuth: MiddlewareFn = (request, response) => {
  const token = request.cookies.get('auth-token')?.value;
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return response;
};

// Compose all middleware
export function middleware(request: NextRequest) {
  return chain([withLogging, withSecurityHeaders, withAuth], request);
}

Limitaciones del Edge Runtime

El Edge Runtime tiene algunas limitaciones.

  • Sin acceso al sistema de archivos
  • Sin módulos nativos Node.js
  • Solo Web APIs
  • Límite de bundle 1MB
  • Sin conexiones DB
  • Tiempo de ejecución limitado
// What you CAN use in Edge Runtime:
// ✅ fetch, Request, Response, Headers, URL
// ✅ TextEncoder, TextDecoder
// ✅ crypto.subtle (Web Crypto API)
// ✅ structuredClone
// ✅ atob, btoa
// ✅ URLPattern
// ✅ EdgeDB, Upstash, PlanetScale drivers

// What you CANNOT use:
// ❌ fs (file system)
// ❌ path, os, child_process
// ❌ pg, mysql2, mongoose (traditional DB drivers)
// ❌ bcrypt (use bcryptjs or Web Crypto instead)
// ❌ Large npm packages that exceed 1MB bundle

Depuración

Técnicas de depuración para middleware.

// Debugging middleware
export function middleware(request: NextRequest) {
  // Log request details
  console.log({
    method: request.method,
    url: request.url,
    pathname: request.nextUrl.pathname,
    cookies: Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value])),
    headers: Object.fromEntries(request.headers),
    geo: request.geo,
    ip: request.ip,
  });

  // Return debug info in response headers (dev only)
  const response = NextResponse.next();
  if (process.env.NODE_ENV === 'development') {
    response.headers.set('x-middleware-pathname', request.nextUrl.pathname);
    response.headers.set('x-middleware-matched', 'true');
  }
  return response;
}

Mejores prácticas

  1. Mantener el middleware ligero.
  2. Excluir archivos estáticos.
  3. Evitar llamadas a DB.
  4. Configurar encabezados de caché.
  5. Cookies para A/B.
  6. Implementar manejo de errores.
  7. Probar diferentes escenarios.
  8. Usar variables de entorno.

Conclusión

El middleware de Next.js es una herramienta poderosa en el borde. Domine estos patrones para aplicaciones robustas.

FAQ

¿El middleware se ejecuta en cada solicitud?

Por defecto sí, use el matcher para limitar.

¿Se puede usar una base de datos?

No directamente. Use fetch a una ruta API.

¿Diferencia entre redirect y rewrite?

Redirect cambia la URL del navegador, rewrite no.

¿Cómo manejar errores?

Envolver en try-catch y devolver una respuesta apropiada.

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

{ }JSON FormatterJWTJWT Decoder

Artículos relacionados

Next.js App Router: Guia de migracion completa 2026

Domina el Next.js App Router con esta guia completa. Server Components, obtencion de datos, layouts, streaming, Server Actions y migracion paso a paso desde Pages Router.

Autenticacion JWT: Guia de implementacion completa

Implementa autenticacion JWT desde cero. Estructura de tokens, access y refresh tokens, implementacion Node.js, gestion en el cliente, mejores practicas de seguridad y middleware Next.js.

React Server Components: Guía Completa 2026

Domina React Server Components: arquitectura, fetching de datos, streaming y migración.