Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
Quick Answer
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
The Error
Your middleware.ts file exists but never runs — requests pass through without being intercepted:
// middleware.ts — this code never executes
export function middleware(request: NextRequest) {
console.log('Middleware running:', request.url); // Never logged
return NextResponse.next();
}Or middleware runs for some routes but not others. Or after adding a matcher, the middleware stops running entirely:
export const config = {
matcher: '/dashboard/:path*', // Middleware stops running after adding this
};Or authentication redirects in middleware are ignored and protected pages render without a session check.
Why This Happens
Next.js middleware has strict requirements for where the file lives and how the matcher is configured:
- Wrong file location —
middleware.tsmust be at the project root (same level asapp/orpages/), not insideapp/,src/app/, or any subdirectory. src/directory projects — if your project uses asrc/directory,middleware.tsmust be insidesrc/, not at the root.- Invalid matcher pattern — a malformed regex or unsupported matcher syntax causes middleware to be silently skipped.
- Missing export — the middleware function must be a named export (
export function middleware) or a default export. A missing or misspelled export means Next.js cannot find it. - Middleware running on static assets — by default, middleware runs on all routes including
_next/static. Without a matcher, it intercepts requests for CSS and JS files, causing performance issues. Adding a matcher fixes this but can accidentally exclude intended routes. - Edge Runtime restrictions — middleware runs in the Edge Runtime, which does not support all Node.js APIs (
fs,crypto,child_process, etc.).
Fix 1: Verify the File Location
my-nextjs-app/
├── app/ ← App Router
│ ├── layout.tsx
│ └── page.tsx
├── middleware.ts ← ✓ CORRECT — at project root
├── next.config.js
└── package.json
# With src/ directory:
my-nextjs-app/
├── src/
│ ├── app/
│ │ └── page.tsx
│ └── middleware.ts ← ✓ CORRECT — inside src/
├── next.config.js
└── package.jsonCommon wrong locations:
app/middleware.ts ← ✗ Wrong — inside app/
pages/middleware.ts ← ✗ Wrong — inside pages/
src/app/middleware.ts ← ✗ Wrong — inside src/app/
middleware/index.ts ← ✗ Wrong — in a subdirectoryCheck where Next.js is finding (or not finding) your middleware:
# Build and check for middleware detection
next build 2>&1 | grep -i middleware
# Or check the .next directory after build
ls .next/server/middleware*.js 2>/dev/null || echo "No middleware compiled"Fix 2: Fix the Matcher Configuration
The matcher config controls which paths trigger middleware. An invalid or overly restrictive matcher silently excludes routes:
Basic matcher patterns:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
console.log('Middleware for:', request.nextUrl.pathname);
return NextResponse.next();
}
export const config = {
matcher: [
// Match a specific path and all sub-paths
'/dashboard/:path*',
// Match multiple paths
'/profile/:path*',
'/settings/:path*',
// Match all paths EXCEPT static files and API routes
'/((?!_next/static|_next/image|favicon.ico|api/).*)',
// Match everything
'/:path*',
],
};The recommended production matcher (excludes static assets):
export const config = {
matcher: [
/*
* Match all request paths EXCEPT:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico
* - Public files (jpg, png, svg, etc.)
*/
'/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};Test your matcher pattern:
# Use the Next.js matcher tester (unofficial)
node -e "
const pattern = '/((?!_next/static|_next/image|favicon\\.ico).*)';
const re = new RegExp(pattern);
const paths = ['/dashboard', '/api/users', '/_next/static/chunk.js', '/favicon.ico'];
paths.forEach(p => console.log(p, '->', re.test(p)));
"Fix 3: Fix Middleware Not Running on API Routes
By default, middleware does not run on API routes unless you explicitly include them in the matcher:
// Middleware NOT running on /api/* routes? Add them to matcher:
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*', // ← Add this to intercept API routes
],
};Or use conditional logic inside middleware:
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Handle API routes differently
if (pathname.startsWith('/api/')) {
// Check API authentication
const token = request.headers.get('authorization');
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
}
// Handle page routes
if (pathname.startsWith('/dashboard')) {
const session = request.cookies.get('session');
if (!session) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};Fix 4: Implement Authentication Redirects Correctly
The most common middleware use case — redirect unauthenticated users:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Routes that require authentication
const protectedRoutes = ['/dashboard', '/profile', '/settings', '/api/user'];
// Routes that should redirect to dashboard if already logged in
const authRoutes = ['/login', '/signup'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const sessionToken = request.cookies.get('session-token')?.value;
const isProtected = protectedRoutes.some(route => pathname.startsWith(route));
const isAuthRoute = authRoutes.some(route => pathname.startsWith(route));
// Redirect to login if accessing protected route without session
if (isProtected && !sessionToken) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname); // Remember where to go after login
return NextResponse.redirect(loginUrl);
}
// Redirect to dashboard if accessing login/signup while already authenticated
if (isAuthRoute && sessionToken) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*',
'/api/user/:path*',
'/login',
'/signup',
],
};Fix 5: Fix Edge Runtime Restrictions
Middleware runs in the Edge Runtime — a restricted environment without full Node.js APIs:
What is NOT available in middleware:
// ✗ These will throw in middleware
import fs from 'fs'; // No filesystem access
import { createHash } from 'crypto'; // No Node.js crypto
import { execSync } from 'child_process'; // No child processes
import prisma from '@/lib/prisma'; // No database connections (TCP)What IS available:
// ✓ These work in middleware
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // JWT verification (Edge-compatible)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (token) {
try {
// jose is Edge-compatible (unlike jsonwebtoken)
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET)
);
// Add user info to request headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.sub as string);
return NextResponse.next({ request: { headers: requestHeaders } });
} catch {
// Invalid token — redirect to login
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.redirect(new URL('/login', request.url));
}Use next/headers in Server Components for database-dependent auth:
// app/dashboard/layout.tsx — Server Component (not middleware)
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { validateSession } from '@/lib/auth'; // Can use Prisma here
export default async function DashboardLayout({ children }) {
const cookieStore = cookies();
const session = cookieStore.get('session-token');
if (!session || !(await validateSession(session.value))) {
redirect('/login');
}
return <>{children}</>;
}Fix 6: Add Debugging to Middleware
Since middleware runs on the server, console.log output appears in the terminal running next dev, not in the browser:
export function middleware(request: NextRequest) {
// Logs appear in your terminal, not the browser console
console.log('[Middleware]', {
url: request.url,
pathname: request.nextUrl.pathname,
method: request.method,
cookies: Object.fromEntries(request.cookies),
});
return NextResponse.next();
}Add a custom response header for debugging (visible in browser DevTools):
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Visible in Network tab → Response Headers
response.headers.set('X-Middleware-Ran', 'true');
response.headers.set('X-Middleware-Path', request.nextUrl.pathname);
return response;
}Check the Network tab in DevTools — if X-Middleware-Ran: true is present in the response, middleware is running. If it’s missing, middleware is not matching the route.
Still Not Working?
Check for a TypeScript compilation error in middleware. If middleware.ts has a type error, Next.js may silently skip it. Run:
npx tsc --noEmitVerify the middleware is compiled in .next:
next build
ls .next/server/middleware.js
# If this file is missing, middleware.ts is not being picked upCheck the Next.js version. Middleware support and the matcher configuration changed significantly across versions:
- Next.js 12 — introduced middleware
- Next.js 13 — renamed
_middleware.tstomiddleware.ts - Next.js 13.1+ — added
matcherconfig with regex support
cat package.json | grep '"next"'
# Ensure you're on 13.1+ for full matcher supportCheck for conflicting next.config.js rewrites. URL rewrites in next.config.js run before middleware. If a rewrite changes the URL path before middleware sees it, the matcher may not match:
// next.config.js — rewrites run BEFORE middleware
module.exports = {
async rewrites() {
return [
{ source: '/old-dashboard', destination: '/dashboard' },
// Middleware sees '/old-dashboard', not '/dashboard' — matcher for '/dashboard' won't fire
];
},
};For related Next.js issues, see Fix: Next.js Build Failed and Fix: Next.js Environment Variables Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Next.js Build Failed (next build Errors and How to Fix Them)
How to fix Next.js build failures — TypeScript errors blocking production builds, module resolution failures, missing environment variables, static generation errors, and common next build crash causes.
Fix: TypeScript Decorators Not Working (experimentalDecorators)
How to fix TypeScript decorators not applying — experimentalDecorators not enabled, emitDecoratorMetadata missing, reflect-metadata not imported, and decorator ordering issues.
Fix: CSS Custom Properties (Variables) Not Working or Not Updating
How to fix CSS custom properties not applying — wrong scope, missing fallback values, JavaScript not setting variables on the right element, and how CSS variables interact with media queries and Shadow DOM.
Fix: TypeScript isolatedModules Errors (const enum, type-only imports)
How to fix TypeScript isolatedModules errors — why const enum fails with Babel and Vite, how to replace const enum, fix re-exported types, and configure isolatedModules correctly for your build tool.