Skip to content

Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)

FixDevs ·

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 locationmiddleware.ts must be at the project root (same level as app/ or pages/), not inside app/, src/app/, or any subdirectory.
  • src/ directory projects — if your project uses a src/ directory, middleware.ts must be inside src/, 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.json

Common 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 subdirectory

Check 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 --noEmit

Verify the middleware is compiled in .next:

next build
ls .next/server/middleware.js
# If this file is missing, middleware.ts is not being picked up

Check the Next.js version. Middleware support and the matcher configuration changed significantly across versions:

  • Next.js 12 — introduced middleware
  • Next.js 13 — renamed _middleware.ts to middleware.ts
  • Next.js 13.1+ — added matcher config with regex support
cat package.json | grep '"next"'
# Ensure you're on 13.1+ for full matcher support

Check 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles