Resolving Next.js 15 Middleware Deprecation Warnings: Migration Guide

⚠️ Understanding the Deprecation

Next.js 15 introduces a significant change to middleware handling, deprecating the _middleware.js file approach in favor of a more flexible Route Handler-based system. If you're seeing warnings like:

Warning: Detected middleware using deprecated API. 
See https://nextjs.org/docs/messages/deprecated-middleware-api for more details.

This guide will help you migrate smoothly.

🔍 What Changed in Next.js 15 Middleware

📦 Old Approach (Deprecated)

// middleware.js or _middleware.js (DEPRECATED in Next.js 15)
export { default } from './middleware';

// OR
export async function middleware(req) {
  return NextResponse.next();
}

✅ New Approach (Recommended)

Next.js 15 introduces two patterns:

  1. Route Handlers (for API-like middleware in app/)
  2. Root middleware.ts (for true edge middleware)

🔄 Migration Strategies

Strategy 1: Convert to Route Handlers (Recommended for API Middleware)

If your middleware was doing API-like functions (authentication, logging, redirects):

Before (pages/ or app/ with _middleware.js)

// _middleware.js
import { NextResponse } from 'next/server';

export async function middleware(req) {
  const { pathname } = req.nextUrl;
  
  // Auth check
  if (pathname.startsWith('/dashboard') && !req.cookies.get('token')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  // Logging
  console.log(`${req.method} ${pathname}`);
  
  return NextResponse.next();
}

After (Route Handlers in app/)

// app/api/auth/route.js
import { NextResponse } from 'next/server';

export async function GET(req) {
  // Protect API routes
  const token = req.cookies.get('token');
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  // Your API logic here
  return NextResponse.json({ message: 'Authenticated' });
}

// app/dashboard/route.js (for page protection)
export async function GET(req) {
  const token = req.cookies.get('token');
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  // Continue to page component (handled by app/dashboard/page.js)
  return NextResponse.next();
}

Strategy 2: Use Root middleware.ts (For True Edge Middleware)

For genuine middleware that needs to run on every request (redirects, headers, logging):

Create middleware.ts in project root

// middleware.ts (placed in project root, same level as app/)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;
  
  // Redirects
  if (pathname.startsWith('/old-blog')) {
    const newPath = pathname.replace('/old-blog', '/blog');
    return NextResponse.redirect(new URL(newPath, req.url));
  }
  
  // Security headers
  const response = NextResponse.next();
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  return response;
}

// Configure matcher to optimize performance
export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public folder
     */
    '/((?!_next/static|_next/image|favicon.ico|public/).*)',
  ],
};

🛠️ Step-by-Step Migration Process

1. Identify Your Current Middleware

# Find all middleware files
find . -name "middleware.js" -o -name "_middleware.js" -o -name "middleware.ts"

2. Analyze What Your Middleware Does

Create a checklist:

  • [ ] Authentication/authorization checks
  • [ ] Redirects/rewrites
  • [ ] Header manipulation
  • [ ] Logging/monitoring
  • [ ] A/B testing or feature flags
  • [ ] Bot blocking or rate limiting
  • [ ] Geolocation or language detection

3. Choose the Right Migration Path

Middleware Function Recommended Approach
API protection Route Handlers in app/
Page protection Route Handlers or Root middleware
Redirects/rewrites Root middleware.ts
Header manipulation Root middleware.ts
Logging/monitoring Root middleware.ts
Bot protection Root middleware.ts
Feature flags Root middleware.ts or Route Handlers

4. Implement the Solution

For Authentication Middleware:

// middleware.ts
export async function middleware(req) {
  const { pathname } = req.nextUrl;
  
  // Public paths that don't need auth
  const publicPaths = ['/login', '/signup', '/api/public', '/_next'];
  
  if (publicPaths.some(path => pathname.startsWith(path))) {
    return NextResponse.next();
  }
  
  // Check auth for protected routes
  const token = req.cookies.get('auth-token');
  if (!token && !pathname.startsWith('/api')) {
    // Redirect to login for pages
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }
  
  // For API routes, return unauthorized
  if (pathname.startsWith('/api') && !token) {
    return NextResponse.json(
      { error: 'Authentication required' }, 
      { status: 401 }
    );
  }
  
  return NextResponse.next();
}

For Logging Middleware:

// middleware.ts
export function middleware(req) {
  const start = Date.now();
  
  const response = NextResponse.next();
  
  // Log after response is sent (non-blocking)
  response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
  
  // Async logging (doesn't block response)
  if (process.env.NODE_ENV === 'production') {
    fetch('/api/log-request', {
      method: 'POST',
      body: JSON.stringify({
        method: req.method,
        path: req.nextUrl.pathname,
        status: response.status,
        timestamp: new Date().toISOString()
      }),
      keepalive: true // Important for edge runtime
    }).catch(() => {}); // Don't let logging errors break the request
  }
  
  return response;
}

🧪 Testing Your Migration

1. Development Testing

# Start dev server and check for warnings
next dev

# Look for: "Warning: Detected middleware using deprecated API"
# Should see NO warnings after migration

2. Build Testing

# Check for build-time warnings
next build

# Should build cleanly without middleware deprecation warnings

3. Production Testing

  1. Deploy to staging environment
  2. Check Vercel/Node.js logs for warnings
  3. Monitor middleware performance
  4. Verify all functionality works (auth, redirects, etc.)

4. Edge Runtime Compatibility

If using root middleware.ts, ensure it works in Edge Runtime:

# Check edge compatibility
next build
# Look for: "info  - Using Edge Runtime for middleware"

🚫 Common Pitfalls to Avoid

❌ Don't Mix Old and New

// DON'T have both _middleware.js AND middleware.ts
// This causes confusion and double execution

❌ Don't Forget Matcher Configuration

Without matcher, middleware runs on EVERY request (including static assets):

// ❌ BAD: Missing matcher - runs on /_next/static/* too
export function middleware(req) { /* ... */ }

// ✅ GOOD: Proper matcher excludes static assets
export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico|public/).*)',
};

❌ Don't Block the Event Loop

// ❌ BAD: Synchronous long-running tasks
export function middleware(req) {
  // This blocks the event loop!
  const hugeArray = new Array(1000000).fill(0);
  hugeArray.map(x => x * 2); // Don't do this in middleware
  
  return NextResponse.next();
}

// ✅ GOOD: Keep middleware lightweight
export function middleware(req) {
  // Fast operations only
  const token = req.cookies.get('token');
  return NextResponse.next();
}

❌ Don't Forget Async/Await for Network Calls

// ❌ BAD: Forgetting await on fetch
export async function middleware(req) {
  fetch('/api/log', { /* ... */ }); // Promise not awaited!
  return NextResponse.next();
}

// ✅ GOOD: Properly await or use fire-and-forget correctly
export async function middleware(req) {
  // For logging, use fire-and-forget correctly
  if (process.env.NODE_ENV === 'production') {
    fetch('/api/log', {
      method: 'POST',
      body: JSON.stringify({ path: req.nextUrl.pathname }),
      keepalive: true
    }).catch(() => {}); // Handle errors gracefully
  }
  
  return NextResponse.next();
}

📊 Performance Considerations

Middleware Performance Budget

  • Target: < 5ms for 95th percentile
  • Critical path: Middleware adds to every request
  • Optimize: Keep it lightweight, avoid expensive operations

Optimization Techniques

  1. Early exits: Return quickly for unmatched paths
  2. Caching: Cache expensive lookups (if appropriate)
  3. Matcher optimization: Be specific about what paths to match
  4. Avoid dependencies: Minimize imported code in middleware
  5. Use built-ins: Leverage Next.js cookies, nextUrl instead of parsing manually

Example Optimization

// ❌ Less optimized: checks all conditions even for static assets
export function middleware(req) {
  const { pathname } = req.nextUrl;
  
  // These run even for /_next/static/css/app.css
  if (pathname.startsWith('/api')) { /* ... */ }
  if (pathname.startsWith('/admin')) { /* ... */ }
  if (pathname.includes('test')) { /* ... */ }
  
  return NextResponse.next();
}

// ✅ More optimized: matcher excludes static assets + early returns
export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico|public/).*)',
};

export function middleware(req) {
  const { pathname } = req.nextUrl;
  
  // Early return for known non-matching paths
  if (pathname.startsWith('/_next') || pathname.startsWith('/public')) {
    return NextResponse.next();
  }
  
  // Now check actual middleware logic
  if (pathname.startsWith('/api')) {
    // API-specific logic
    return NextResponse.next();
  }
  
  if (pathname.startsWith('/admin')) {
    // Admin-specific logic
    return NextResponse.next();
  }
  
  return NextResponse.next();
}

🔧 Verification Checklist

Before considering your migration complete:

✅ Functional Checks

  • [ ] All authentication redirects work correctly
  • [ ] API routes return proper status codes (401/403 when needed)
  • [ ] Redirects preserve query parameters correctly
  • [ ] Headers are set as expected
  • [ ] Logging/monitoring still functions
  • [ ] Bot blocking/rate limiting still works
  • [ ] Feature flags work correctly

✅ Technical Checks

  • [ ] No deprecated middleware warnings in dev console
  • [ ] Clean build with next build
  • [ ] Middleware shows as using Edge Runtime (when applicable)
  • [ ] Matcher configuration is correct and tested
  • [ ] No infinite redirect loops
  • [ ] Cookie handling works correctly
  • [ ] Serverless function compatibility (if applicable)

✅ Performance Checks

  • [ ] Middleware execution time < 5ms (measure with x-response-time header)
  • [ ] No memory leaks in middleware
  • [ ] Proper error handling (doesn't crash on malformed input)
  • [ ] Works correctly with preview deployments
  • [ ] Handles edge cases (large URLs, unusual headers, etc.)

🔄 Rollback Plan

If issues arise after migration:

1. Quick Revert

# Temporarily rename new middleware
mv middleware.ts middleware.ts.bak

# Restore old middleware if you backed it up
# Or comment out new middleware and restore old logic

2. Partial Migration

Run both systems temporarily during transition:

// middleware.ts - Keep old functionality running alongside new
export function middleware(req) {
  // Run new logic
  const newResponse = runNewMiddlewareLogic(req);
  
  // Run old logic for comparison (remove after verification)
  if (process.env.NODE_ENV === 'development') {
    runOldMiddlewareLogic(req); // Just for logging/comparison
  }
  
  return newResponse;
}

📚 Resources


Middleware migration in Next.js 15 is about choosing the right tool for the job: Route Handlers for API-like logic, root middleware.ts for true edge capabilities. Follow the matcher patterns and performance guidelines for a smooth transition.