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:
- Route Handlers (for API-like middleware in app/)
- 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
- Deploy to staging environment
- Check Vercel/Node.js logs for warnings
- Monitor middleware performance
- 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
- Early exits: Return quickly for unmatched paths
- Caching: Cache expensive lookups (if appropriate)
- Matcher optimization: Be specific about what paths to match
- Avoid dependencies: Minimize imported code in middleware
- 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-timeheader) - [ ] 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
- Next.js 15 Middleware Documentation
- Migration Guide from Next.js 14 to 15
- Edge Runtime Documentation
- Route Handlers vs Middleware
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.