Guía práctica de Next.js Middleware: coincidencia de rutas, limitaciones de Edge Runtime y errores comunes

Los logs de error de Vercel. Acabábamos de desplegar el panel de administración: en local, todas las rutas /dashboard estaban protegidas y los usuarios no autenticados iban al login. En producción, alguien entró directo a /dashboard/settings/profile, saltó la verificación y vio datos sensibles.
Abrí el código: el archivo Middleware estaba ahí y la lógica parecía correcta. ¿Qué pasaba?
Tras media hora en la documentación de Next.js, la respuesta estaba en la sección del matcher: yo tenía /dashboard/:path, que solo cubre un nivel como /dashboard/settings. Las rutas de varios niveles no coincidían. La forma correcta es /dashboard/:path*. Ese asterisco casi me hace cargar con la culpa.
En pocas palabras, no es la primera vez que Middleware me hace tropezar. Edge Runtime que no soporta una librería, matcher que no aplica, bucles infinitos de redirección… los he pisado casi todos.
Si usas Next.js Middleware o piensas usarlo para autenticación, i18n o pruebas A/B, este artículo puede ayudarte. Desgloso las trampas habituales, las reglas de configuración que cuestan entender y tres casos completos con código.
Sin relleno del tipo «en el desarrollo web actual». Solo problemas reales, cómo escribirlo, cómo evitar errores y cómo hacer que Middleware funcione de verdad.
¿Qué es Middleware y para qué sirve?
En resumen, Middleware es un «punto de control».
Antes de que la petición llegue a tu página o API, pasa por ahí. Puedes comprobar identidad, modificar la petición o devolver una respuesta directamente: mandar al login a quien no esté autenticado o redirigir según la región del usuario.
Corre en Edge Runtime, y eso importa. No corre en tu servidor, sino en nodos edge cerca del usuario (CDN). Menos latencia, arranque en frío casi nulo. Middleware es la capa de código más cercana al usuario.
¿Qué diferencia hay entre Edge Runtime y Node.js Runtime?
| Característica | Edge Runtime | Node.js Runtime |
|---|---|---|
| Velocidad de arranque | Casi cero cold start | Cientos de milisegundos |
| Ubicación | Nodos edge globales | Servidor concreto |
| APIs soportadas | APIs web estándar | APIs completas de Node.js |
| Casos de uso | Lógica ligera, respuesta rápida | Cálculo complejo, base de datos |
En pocas palabras: rápido, pero limitado. No puedes usar módulos como fs o path, ni conectar la mayoría de bases de datos. Es donde más suele tropezar la gente.
¿Cuándo conviene usar Middleware?
No toda la lógica encaja aquí. Estos son los casos más habituales:
1. Autenticación (Auth Gate)
El clásico: comprobar si el usuario está logueado y redirigir al login si no. Más rápido que hacerlo en Server Components, porque la petición aún no llega al servidor.
2. Internacionalización (i18n)
Según preferencia de idioma (URL, cookie o navegador), redirigir a la versión correcta. / → /zh o /en.
3. Pruebas A/B
Repartir usuarios al azar en dos grupos con versiones distintas. Cookie para mantener el grupo y evitar cambios al refrescar.
4. Detección de bots y rate limiting
Bloquear crawlers o peticiones maliciosas, o limitar frecuencia por IP.
5. Logs y analítica
Registrar path, referrer, UA y enviar a un servicio de análisis.
6. Reescritura de contenido (Rewrite)
Mapear internamente una URL a otra sin cambiar la barra de direcciones. Muy útil en rutas dinámicas o A/B.
¿Por qué no hacerlo en los componentes de página?
Se puede, pero es más lento. La lógica en Server o Client Components corre cuando la petición ya llegó al servidor o incluso durante el render. Middleware intercepta en el edge: respuesta más rápida, mejor experiencia.
Además, centralizas: no quieres repetir autenticación en cada página protegida. Un solo sitio lo resuelve.
Pero no metas todo en Middleware. Lógica de negocio pesada, consultas a base de datos o mucho cálculo van mejor en rutas API o Server Components. Middleware debe ser ligero y rápido.
Configuración básica y estructura de archivos
¿Dónde va el archivo?
Next.js es estricto: en la raíz del proyecto o en src, y el nombre debe ser middleware.ts (o .js).
Raíz del proyecto/
├── app/
├── middleware.ts ← aquí
├── package.json
O con carpeta src:
Raíz del proyecto/
├── src/
│ ├── app/
│ ├── middleware.ts ← aquí
├── package.json
Nota: solo puede haber un middleware.ts por proyecto. No crees varios en app u otras carpetas. Tiene sentido: Middleware es el «portero» global.
El Middleware más simple
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
console.log('Petición entrante:', request.url);
return NextResponse.next(); // dejar pasar
}
Así de simple. NextResponse.next() significa «todo bien, continúa» hacia la página o API destino.
APIs principales: NextRequest y NextResponse
NextRequest extiende el Request web estándar con utilidades:
request.nextUrl: URL parseada (pathname, search, etc.)request.cookies: leer y escribir cookies con comodidadrequest.geo: ubicación del usuario (si la plataforma lo soporta, p. ej. Vercel)
NextResponse ofrece varias formas de responder:
1. Dejar pasar
return NextResponse.next();
2. Redirigir (URL nueva visible)
return NextResponse.redirect(new URL('/login', request.url));
3. Reescribir (URL interna, barra sin cambios)
return NextResponse.rewrite(new URL('/dashboard/v2', request.url));
El usuario ve /dashboard pero recibe contenido de /dashboard/v2. Muy útil en A/B o cambio de versión.
4. Respuesta directa
return new NextResponse('Acceso denegado', { status: 403 });
Sin continuar el pipeline.
Ejemplo un poco más útil: header personalizado
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
Útil para timestamp en todas las respuestas o marcar origen de la petición.
Nota de versión (importante)
En Next.js 15, oficialmente renombraron middleware.ts a proxy.ts. middleware.ts sigue funcionando (compatibilidad). Proyectos nuevos pueden usar el nombre nuevo. Este artículo aplica a Next.js 14/15; conceptos y APIs son los mismos.
Coincidencia de rutas (Matcher): donde más se tropieza
Siendo honesto, el matcher es donde más he fallado.
¿Por qué hace falta el matcher?
Sin matcher, Middleware se ejecuta en cada petición, incluidos CSS, JS, imágenes y fuentes. Una página puede disparar 20 ejecuciones solo por assets. Desperdicio y posible lentitud.
El matcher le dice a Next.js: «solo en estas rutas, el resto ignóralo».
Sintaxis básica
Exporta config en middleware.ts:
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}
Solo rutas bajo /dashboard y /api ejecutan Middleware.
Modificadores: *, +, ?
Controlan cuánto «path» se consume:
* (cero o más)
/dashboard/:path* coincide con:
/dashboard✓/dashboard/settings✓/dashboard/settings/profile✓
+ (uno o más)
/dashboard/:path+ coincide con:
/dashboard✗/dashboard/settings✓/dashboard/settings/profile✓
? (cero o uno)
/dashboard/:path? coincide con:
/dashboard✓/dashboard/settings✓/dashboard/settings/profile✗
En la mayoría de casos, * basta.
Trampas habituales y soluciones (importante)
Tabla que me costó aprender; conviene guardarla:
| Problema | Incorrecto | Correcto | Motivo |
|---|---|---|---|
| Varios niveles no coinciden | /dashboard/:path | /dashboard/:path* | Sin * solo un nivel |
| Se olvida la raíz | matcher: ['/dashboard/:path*'] | matcher: ['/', '/dashboard/:path*'] | / no se incluye solo |
| Assets estáticos interceptados | matcher: ['/:path*'] | matcher: ['/((?!_next|favicon.ico).*)'] | Excluir _next con regex |
| API sin proteger | matcher: ['/api/users'] | matcher: ['/api/:path*'] | Ruta concreta solo cubre una |
La trampa de los recursos estáticos
Matcher demasiado amplio:
export const config = {
matcher: ['/:path*'] // todas las rutas
}
También coincide _next/static, Middleware se dispara en exceso y la página va lenta.
Solución: lookahead negativo
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Significa: «todo excepto api, _next/static, _next/image y favicon.ico».
La regex es enrevesada; yo copié el ejemplo oficial. Úsala tal cual.
Otro error: no puedes usar valores dinámicos
const lang = 'zh';
export const config = {
matcher: [`/${lang}/:path*`] // ❌ no funciona
}
El matcher debe ser estático, conocido en compilación. Sin variables en plantillas ni generación en runtime.
La lógica dinámica va dentro de middleware:
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
// lógica
}
return NextResponse.next();
}
export const config = {
matcher: ['/:locale/:path*']
}
Plantillas de matcher que recomiendo
Proteger rutas concretas (admin):
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
Todo excepto estáticos:
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)',
],
}
Proteger todas las API:
export const config = {
matcher: ['/api/:path*']
}
La documentación de Next.js sobre matcher es breve; mucho hay que descubrirlo a prueba y error. Espero que esta tabla te ahorre tiempo.
Limitaciones de Edge Runtime y cómo sortearlas
Este capítulo me dejó perplejo la primera vez.
Quería validar identidad en Middleware consultando la base de datos. Al ejecutar en local: Native Node.js APIs are not supported in the Edge Runtime.
¿Cómo que no puedo conectar a la base de datos?
Edge Runtime no es Node.js completo; muchas APIs y librerías habituales no están disponibles.
¿Qué no soporta Edge Runtime?
| Categoría | APIs/módulos no soportados | Impacto |
|---|---|---|
| Sistema de archivos | fs, path | Sin lectura/escritura local |
| Subprocesos | child_process | Sin comandos externos |
| Cifrado | Parte de crypto | Usar Web Crypto API |
| Base de datos | Drivers nativos MongoDB, MySQL | Mayoría de drivers tradicionales no disponibles |
| Otros | process.emit, setImmediate | Algunas APIs de bajo nivel de Node.js |
¿Qué implica en la práctica?
- No puedes consultar la base de datos directamente para autenticación
- No puedes leer config desde
config.json - No puedes usar librerías que dependan de APIs de Node.js
Suena limitante, pero hay formas de rodearlo.
Estrategia: Edge Runtime como «avanzada»
Idea clave: Middleware solo hace comprobaciones ligeras; lo pesado va después.
| Necesidad | ❌ Limitación Edge | ✅ Solución |
|---|---|---|
| Validar identidad | Sin consulta a BD | JWT local o llamada a ruta API |
| Cifrado | crypto parcial | Web Crypto API |
| Leer config | Sin filesystem | process.env o API |
| Logs | Sin archivos locales | Servicio externo (Logtail, etc.) |
| Base de datos | Drivers tradicionales | BD compatible con Edge (Vercel Postgres, Supabase) |
Ejemplo: validación JWT (recomendado)
JWT es ideal en Middleware porque es sin estado: el token lleva la información, sin consultar BD.
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // compatible con Edge Runtime
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 secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
const response = NextResponse.next();
response.headers.set('x-user-id', payload.userId as string);
return response;
} catch (error) {
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*']
}
Puntos clave:
- Usa
jose, nojsonwebtoken(este depende decryptode Node.js) - El secret JWT desde
process.env(disponible en Edge) - Si falla la validación, borra la cookie
¿Y si necesitas base de datos sí o sí?
Por ejemplo, comprobar si un usuario está suspendido. Puedes llamar a una ruta API desde Middleware:
export async function middleware(request: NextRequest) {
const userId = request.cookies.get('user-id')?.value;
if (!userId) {
return NextResponse.redirect(new URL('/login', request.url));
}
const apiUrl = new URL('/api/check-user-status', request.url);
const response = await fetch(apiUrl, {
headers: { 'x-user-id': userId }
});
const { isActive } = await response.json();
if (!isActive) {
return NextResponse.redirect(new URL('/account-suspended', request.url));
}
return NextResponse.next();
}
La ruta API corre en Node.js y puede usar BD. Añade latencia; úsalo solo cuando haga falta.
Sobre Web Crypto API
Para cifrado/hash, la API nativa del navegador:
const data = new TextEncoder().encode('hello world');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
Más incómoda que crypto de Node.js, pero es lo que hay en Edge.
¿Cómo saber si una librería soporta Edge?
Documentación o prueba directa. Si sale Native Node.js APIs are not supported, no va.
Alternativas habituales:
- JWT:
joseen lugar dejsonwebtoken - BD: Vercel Postgres, Supabase, Prisma (parcial)
- Logs: Logtail, Axiom
Mi consejo: no hagas cosas complejas en Middleware. Entra, decide y sale; lo pesado en rutas API.
Casos prácticos: tres escenarios con código completo
Bastante teoría; toca código. Tres casos reales que he usado; puedes copiarlos.
Caso 1: autenticación y protección de rutas
Escenario: panel admin; todo bajo /dashboard requiere login.
Código completo:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const token = request.cookies.get('auth-token')?.value;
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
const expiresAt = payload.exp as number;
const now = Math.floor(Date.now() / 1000);
const shouldRefresh = expiresAt - now < 3600;
const response = NextResponse.next();
if (shouldRefresh) {
response.headers.set('x-token-refresh-needed', 'true');
}
response.headers.set('x-user-id', payload.userId as string);
response.headers.set('x-user-role', payload.role as string);
return response;
} catch (error) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
loginUrl.searchParams.set('reason', 'expired');
const response = NextResponse.redirect(loginUrl);
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*']
}
Puntos clave:
- Parámetro
frompara volver tras el login - Comprobar expiración próxima y refrescar antes de que caiga la sesión
- Info de usuario en headers (opcional)
Pruebas:
- Sin cookie, visitar
/dashboard→/login?from=/dashboard - Con cookie válida → página normal
Problema frecuente:
- Síntoma: funciona en local, no en producción
- Causa: falta
JWT_SECRETen producción - Solución: configurar variable en Vercel/Netlify
Caso 2: redirección i18n
Escenario: sitio en chino e inglés; en / redirigir a /zh o /en según preferencia.
Código completo:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const supportedLocales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';
function getPreferredLocale(request: NextRequest): string {
const urlLocale = request.nextUrl.searchParams.get('lang');
if (urlLocale && supportedLocales.includes(urlLocale)) {
return urlLocale;
}
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && supportedLocales.includes(cookieLocale)) {
return cookieLocale;
}
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const browserLang = acceptLanguage.split(',')[0].split('-')[0];
if (supportedLocales.includes(browserLang)) {
return browserLang;
}
}
return defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = supportedLocales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
const locale = getPreferredLocale(request);
const newUrl = new URL(`/${locale}${pathname}`, request.url);
newUrl.search = request.nextUrl.search;
const response = NextResponse.redirect(newUrl);
response.cookies.set('NEXT_LOCALE', locale, {
maxAge: 60 * 60 * 24 * 30,
path: '/'
});
return response;
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)'
]
}
Puntos clave:
- Prioridad: parámetro URL > cookie > navegador
- Cookie recuerda la elección
- Matcher excluye estáticos
Integración con next-intl:
import { createI18nMiddleware } from 'next-intl/middleware';
export default createI18nMiddleware({
locales: ['en', 'zh', 'ja'],
defaultLocale: 'en'
});
export const config = {
matcher: ['/((?!api|_next|.*\\.).)']
};
next-intl gestiona detección y cookies.
Caso 3: pruebas A/B y feature flags
Escenario: nueva home; 50 % de usuarios ven la versión nueva antes del rollout completo.
Código completo:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname !== '/') {
return NextResponse.next();
}
let variant = request.cookies.get('ab-test-homepage')?.value;
if (!variant) {
variant = Math.random() < 0.5 ? 'A' : 'B';
}
let response: NextResponse;
if (variant === 'B') {
response = NextResponse.rewrite(new URL('/homepage-v2', request.url));
} else {
response = NextResponse.next();
}
response.cookies.set('ab-test-homepage', variant, {
maxAge: 60 * 60 * 24 * 7,
path: '/'
});
response.headers.set('x-ab-variant', variant);
return response;
}
export const config = {
matcher: ['/']
}
Puntos clave:
rewrite, noredirect: la URL sigue siendo/- Cookie fija el grupo
- Header
x-ab-variantpara analítica
Tracking en la página:
// app/page.tsx
import { headers } from 'next/headers';
export default function HomePage() {
const headersList = headers();
const abVariant = headersList.get('x-ab-variant');
useEffect(() => {
analytics.track('page_view', {
page: 'homepage',
variant: abVariant
});
}, [abVariant]);
return <div>...</div>;
}
Avanzado: agrupar por user ID
const userId = request.cookies.get('user-id')?.value;
if (userId) {
const hash = simpleHash(userId);
variant = hash % 2 === 0 ? 'A' : 'B';
} else {
variant = request.cookies.get('ab-test-homepage')?.value ||
(Math.random() < 0.5 ? 'A' : 'B');
}
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;
}
return Math.abs(hash);
}
Estos tres casos cubren lo más habitual. Puedes combinarlos (auth + i18n, por ejemplo).
Optimización del rendimiento y buenas prácticas
Escribir Middleware es fácil; hacerlo bien requiere cuidado.
Organizar la lógica en módulos
Con el proyecto grande, un solo archivo se vuelve caótico. Next.js solo permite un middleware.ts, pero puedes dividir funciones.
Estructura recomendada:
Raíz del proyecto/
├── middleware/
│ ├── auth.ts # autenticación
│ ├── i18n.ts # i18n
│ ├── ab-test.ts # A/B
│ └── rate-limit.ts # rate limiting
├── middleware.ts # entrada
Archivo de entrada:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from './middleware/auth';
import { handleI18n } from './middleware/i18n';
import { handleABTest } from './middleware/ab-test';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const i18nResponse = handleI18n(request);
if (i18nResponse) return i18nResponse;
if (pathname.startsWith('/dashboard')) {
const authResponse = await checkAuth(request);
if (authResponse) return authResponse;
}
if (pathname === '/') {
return handleABTest(request);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
Ejemplo auth.ts:
// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
export async function checkAuth(request: NextRequest): Promise<NextResponse | null> {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
await jwtVerify(token, secret);
return null;
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
Cada módulo con responsabilidad clara.
Caché: menos cálculo repetido
Algunas comprobaciones se pueden cachear. Ejemplo: permisos mientras el token siga válido.
Vercel Edge Config:
import { get } from '@vercel/edge-config';
export async function middleware(request: NextRequest) {
const featureFlags = await get('feature-flags');
if (featureFlags?.newDashboard) {
return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
}
return NextResponse.next();
}
Edge Config es KV distribuido global; ideal para flags que cambian poco.
Evita headers demasiado grandes
Headers de Middleware van en la respuesta. Demasiado tamaño → 431 Request Header Fields Too Large.
Recomendaciones:
- Mantener headers bajo 8 KB
- Solo datos necesarios; no serialices el objeto usuario entero
- Muchos datos → token cifrado
Mal ejemplo:
// ❌ no hagas esto
response.headers.set('x-user-data', JSON.stringify(userData));
Mejor:
// ✅ solo lo esencial
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);
Matcher: precisión sobre comodines
Matcher más específico = menos trabajo para Next.js.
Menos ideal:
export const config = {
matcher: ['/:path*']
}
Mejor:
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}
Si solo proteges unas rutas, no uses comodín global.
Monitorización y depuración
En desarrollo:
export function middleware(request: NextRequest) {
if (process.env.NODE_ENV === 'development') {
console.log('Middleware ejecutado:', {
path: request.nextUrl.pathname,
method: request.method,
cookies: request.cookies.getAll()
});
}
// tu lógica...
}
En producción:
console.log en Edge va a logs de la plataforma (p. ej. Vercel). No abuses; suma tiempo.
Logs externos:
import { Logger } from '@logtail/edge';
const logger = new Logger(process.env.LOGTAIL_TOKEN);
export async function middleware(request: NextRequest) {
try {
// lógica...
} catch (error) {
await logger.error('Middleware error', {
path: request.nextUrl.pathname,
error: error.message
});
throw error;
}
}
Checklist de buenas prácticas
✅ Haz:
- Mantén Middleware ligero; lo complejo en rutas API
- Matcher preciso
- JWT para auth, evita BD en el edge
- Módulos por función
- Logs de depuración en desarrollo
- Cookies con expiración razonable
❌ Evita:
- Cálculo pesado o BD en Middleware
- Sin matcher (todas las peticiones)
- Librerías que requieren Node.js
- Headers > 8 KB
- Muchos logs en producción
- Bucles infinitos de redirección
Errores habituales y depuración
Errores que dan ganas de tirar el portátil. Los he vivido.
Error 1: Middleware no se ejecuta
Síntoma: escribiste Middleware pero no pasa nada.
| Causa | Cómo comprobar | Solución |
|---|---|---|
| Ubicación incorrecta | ¿middleware.ts en raíz o src? | Mover al sitio correcto |
| Matcher no cubre la ruta | Imprime request.nextUrl.pathname | Ajustar matcher |
| Error de sintaxis | Consola de compilación | Corregir |
| Export incorrecto | ¿export function middleware? | Revisar export |
| Caché | Borrar .next | rm -rf .next && npm run dev |
Depuración:
export function middleware(request: NextRequest) {
console.log('🔥 Middleware ejecutado. Ruta:', request.nextUrl.pathname);
// resto...
}
Si no aparece el log, Middleware no corre; revisa la tabla.
Error 2: Native Node.js APIs are not supported in the Edge Runtime
Síntoma: error en runtime sobre API o módulo no soportado.
Causas frecuentes:
fs,pathjsonwebtoken(usajose)- Drivers nativos MongoDB/MySQL
Soluciones:
- Librería alternativa compatible con Edge
- Mover lógica a ruta API
- Web Crypto API en lugar de
cryptode Node.js
// ❌ no
import jwt from 'jsonwebtoken';
// ✅ sí
import { jwtVerify } from 'jose';
Error 3: Invalid middleware found
Al arrancar el proyecto.
1. matcher vacío
export const config = {
matcher: []
}
2. Sin export de middleware
const middleware = (request: NextRequest) => { ... }
// debe ser:
export function middleware(request: NextRequest) { ... }
3. Sin return
export function middleware(request: NextRequest) {
console.log('algo');
// falta return
}
// debe:
export function middleware(request: NextRequest) {
return NextResponse.next();
}
Error 4: bucle infinito de redirección
Síntoma: ERR_TOO_MANY_REDIRECTS.
Causa: el destino vuelve a disparar Middleware.
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*'] // incluye /login
}
Solución: excluir páginas públicas
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === '/login' || pathname === '/') {
return NextResponse.next();
}
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
O ajustar matcher:
export const config = {
matcher: ['/dashboard/:path*']
}
Error 5: variables de entorno undefined
Síntoma: process.env.XXX es undefined.
Causas:
- Falta
.env.localo nombre mal escrito - Plataforma de despliegue sin la variable
- Nombre no válido para Next.js
Local:
// .env.local
JWT_SECRET=your-secret-here
Producción: configurar en Vercel/Netlify y redesplegar.
Nota:
- Reinicia el servidor de desarrollo tras cambiar env
- En cliente hace falta prefijo
NEXT_PUBLIC_
Resumen de depuración
1. Headers de seguimiento
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-middleware-executed', 'true');
response.headers.set('x-middleware-path', request.nextUrl.pathname);
return response;
}
Revisa headers de respuesta en DevTools.
2. Comentar por partes
export function middleware(request: NextRequest) {
console.log('Paso 1');
// ...
console.log('Paso 2');
// ...
console.log('Paso 3');
return NextResponse.next();
}
El último log indica dónde falla.
3. Logs de Vercel Edge Functions
En Functions del proyecto ves console.log y errores.
4. next dev --turbo
Next.js 15+ con Turbo: arranque más rápido y errores más claros.
npm run dev -- --turbo
No entres en pánico; paso a paso suele bastar. GitHub Discussions y Stack Overflow tienen muchos casos ya resueltos.
Conclusión
Tres ideas finales:
1. Límites claros de Middleware
No metas todo. Sirve para lo ligero: auth, redirecciones, A/B. Negocio complejo, BD y cálculo pesado → rutas API o Server Components. Middleware es portero, no mayordomo.
2. El matcher lo es todo
Donde más se falla. Rutas dinámicas con *, excluir estáticos, no interceptar login. Usa las plantillas de este artículo o loguea rutas en desarrollo.
3. Acepta las limitaciones de Edge Runtime
No es Node.js completo: velocidad y distribución global a cambio de menos APIs. JWT, Web Crypto API y llamadas a API te permiten trabajar bien dentro de eso.
Next.js Middleware no es difícil; son muchos detalles. Matcher, Edge, bucles de redirección… los he pisado yo; escribo esto para que tú no repitas.
Si necesitas interceptación global, prueba Middleware. Cuando funcione, optimiza. Si algo falla, vuelve a la sección de errores.
Si te sirvió, compártelo con quien use Next.js. El siguiente tema que tengo en mente son Server Actions — otro campo minado.
Ánimo: que tu Middleware funcione a la primera.
Flujo completo de configuración de Next.js Middleware
Desde crear el archivo Middleware hasta implementar protección de rutas, i18n y pruebas A/B
⏱️ Estimated time: 3 hr
- 1
Step 1: Crear el archivo Middleware
Crear el archivo:
• Ubicación: middleware.ts (raíz del proyecto)
• Exportar el objeto config con el matcher
• Exportar la función middleware para procesar peticiones
Estructura básica:
export const config = {
matcher: '/dashboard/:path*'
}
export function middleware(request: NextRequest) {
// lógica de procesamiento
} - 2
Step 2: Configurar reglas del matcher
Reglas de coincidencia:
• Ruta única: '/dashboard'
• Ruta dinámica: '/dashboard/:path*' (el asterisco cubre varios niveles)
• Varias rutas: ['/dashboard/:path*', '/admin/:path*']
• Excluir rutas: lookahead negativo, p. ej. '/((?!api|_next/static|_next/image|favicon.ico).*)'
Notas:
• Las rutas dinámicas necesitan * para varios niveles
• Excluye recursos estáticos (_next/static, _next/image, etc.)
• No interceptes páginas públicas (login, etc.) - 3
Step 3: Implementar protección de rutas (autenticación)
Pasos:
1. Leer el token de la cookie
2. Validar el token (JWT o llamada a API)
3. Redirigir usuarios no autenticados al login
4. Dejar pasar a usuarios autenticados
Puntos clave:
• NextRequest.cookies para leer cookies
• NextResponse.redirect para redirigir
• NextResponse.next para continuar
• Evita bucles infinitos de redirección - 4
Step 4: Implementar internacionalización (cambio de idioma)
Pasos:
1. Detectar preferencia de idioma (cookie, header, valor por defecto)
2. Decidir si hace falta redirigir según la ruta
3. Añadir prefijo de idioma a la URL
4. Guardar cookie de idioma
Puntos clave:
• request.headers.get('accept-language')
• request.nextUrl.pathname para la ruta
• NextResponse.rewrite para reescribir la URL
• Permite cambiar idioma sin alterar la estructura visible de la URL - 5
Step 5: Gestionar limitaciones de Edge Runtime
Limitaciones y soluciones:
• Sin APIs de Node.js → usa APIs web estándar
• Sin sistema de archivos → variables de entorno o llamadas a API
• Algunos paquetes npm no compatibles → comprueba soporte Edge
• Validación JWT → Web Crypto API o ruta API
Depuración:
• console.log para depurar
• Revisa logs de Vercel Edge Functions
• try-catch para capturar errores - 6
Step 6: Probar y depurar
Qué probar:
• Todas las rutas que coinciden
• Rutas no coincidentes (no deben interceptarse)
• Lógica de redirección
• Compatibilidad con Edge Runtime
Depuración:
• console.log en middleware
• Pestaña Network del navegador
• Logs de Vercel Edge Functions
• Modo desarrollo de Next.js para ver avisos
FAQ
¿Qué hacer si la configuración del matcher de Middleware no funciona?
1) Si la ruta del matcher es correcta (rutas dinámicas necesitan *)
2) Si excluiste recursos estáticos
3) Si el formato de ruta es el soportado por Next.js (no uses regex propia)
Puedes añadir console.log en middleware para ver qué rutas coinciden.
¿Por qué no coinciden rutas de varios niveles?
¿Qué hacer si Edge Runtime no soporta una librería?
Soluciones:
1) Comprobar si el paquete soporta Edge Runtime
2) Usar APIs web estándar como alternativa
3) Mover lógica compleja a rutas API o Server Components
4) Usar librerías alternativas compatibles con Edge
¿Cómo evitar bucles infinitos de redirección?
¿Se puede acceder a la base de datos desde Middleware?
Si necesitas consultar la base de datos:
1) Llama a una ruta API desde Middleware
2) Usa variables de entorno para configuración
3) Mueve lógica compleja a rutas API o Server Components
¿Cómo depurar Middleware?
1) Añade console.log en la función middleware
2) Revisa la pestaña Network del navegador
3) Consulta logs de Vercel Edge Functions
4) Usa el modo desarrollo de Next.js para avisos y errores
¿Cuál es la diferencia entre Middleware y rutas API?
• Corre en Edge Runtime
• Se ejecuta antes de que la petición llegue a la página o ruta API
• Ideal para interceptación y reenvío ligeros
Rutas API:
• Corren en runtime Node.js
• Acceso completo a APIs de Node.js y base de datos
• Ideal para lógica de negocio compleja
18 min de lectura · Publicado el: 25 dic 2025 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Tutorial de Next.js Server Actions: mejores prácticas para formularios y validación
Guía práctica sobre formularios con Next.js Server Actions: validación con Zod, seguridad y optimización de UX para dominar esta función que simplifica el flujo de desarrollo
Parte 10 de 51
Siguiente
Protección de rutas y control de permisos en Next.js: guía completa de Middleware y defensa en profundidad
Análisis profundo de la protección de rutas y el control de permisos en Next.js, desde Middleware hasta una arquitectura de defensa en profundidad, con NextAuth y getServerSession para un sistema RBAC seguro y ejemplos de código completos.
Parte 12 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario