Skip to main content
Ganesh Joshi
Back to Blogs

Edge middleware in Next.js: run logic before the request

February 18, 20265 min read
Tutorials
Middleware or routing code on screen

Next.js middleware runs at the edge, before a request hits your page or API routes. It is ideal for auth checks, redirects, rewrites, and header manipulation. Middleware runs in the Edge Runtime for low latency and global distribution.

Creating middleware

Create middleware.ts at the project root:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Your logic here
  return NextResponse.next();
}

The file must be named middleware.ts (or .js) and placed at the root, next to app/ or pages/.

Matching paths

Default behavior

Without configuration, middleware runs on every request. This is usually not what you want.

Using matcher

// middleware.ts
export function middleware(request: NextRequest) {
  // ...
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/protected/:path*',
  ],
};

Exclude static files

export const config = {
  matcher: [
    // Match all except static files and Next.js internals
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Matcher patterns

Pattern Matches
/dashboard Exactly /dashboard
/dashboard/:path /dashboard/anything
/dashboard/:path* /dashboard and all subpaths
/api/:path+ /api with at least one segment
/(dashboard|admin)/:path* /dashboard/* or /admin/*

Common patterns

Authentication

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');
  const isAuthPage = request.nextUrl.pathname.startsWith('/login');
  const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');

  // Redirect to login if not authenticated
  if (isProtectedRoute && !token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Redirect to dashboard if already authenticated
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};

Redirects

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Redirect old URLs
  if (pathname === '/old-page') {
    return NextResponse.redirect(new URL('/new-page', request.url));
  }

  // Redirect www to non-www
  if (request.headers.get('host')?.startsWith('www.')) {
    const url = request.nextUrl.clone();
    url.host = url.host.replace('www.', '');
    return NextResponse.redirect(url, 301);
  }

  return NextResponse.next();
}

URL rewrites

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Rewrite /blog/123 to /posts/123 (URL stays as /blog/123)
  if (pathname.startsWith('/blog/')) {
    const slug = pathname.replace('/blog/', '');
    return NextResponse.rewrite(new URL(`/posts/${slug}`, request.url));
  }

  return NextResponse.next();
}

A/B testing

export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value;
  
  if (!bucket) {
    const newBucket = Math.random() < 0.5 ? 'control' : 'variant';
    const response = NextResponse.next();
    response.cookies.set('ab-bucket', newBucket, {
      maxAge: 60 * 60 * 24 * 7, // 1 week
    });
    return response;
  }

  // Rewrite to variant page
  if (bucket === 'variant' && request.nextUrl.pathname === '/pricing') {
    return NextResponse.rewrite(new URL('/pricing-variant', request.url));
  }

  return NextResponse.next();
}

Geolocation

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';
  const city = request.geo?.city;
  
  // Redirect to country-specific page
  if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
    return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url));
  }

  // Add geo headers for downstream use
  const response = NextResponse.next();
  response.headers.set('x-user-country', country);
  if (city) response.headers.set('x-user-city', city);
  
  return response;
}

Localization

const locales = ['en', 'de', 'fr', 'es'];
const defaultLocale = 'en';

function getLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language');
  if (!acceptLanguage) return defaultLocale;
  
  const preferred = acceptLanguage.split(',')[0].split('-')[0];
  return locales.includes(preferred) ? preferred : defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Check if pathname has locale
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return NextResponse.next();

  // Redirect to locale-prefixed path
  const locale = getLocale(request);
  return NextResponse.redirect(
    new URL(`/${locale}${pathname}`, request.url)
  );
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
};

Adding headers

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // Security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Custom headers
  response.headers.set('x-request-id', crypto.randomUUID());
  
  return response;
}

Working with cookies

export function middleware(request: NextRequest) {
  // Read cookies
  const sessionId = request.cookies.get('session-id')?.value;
  const allCookies = request.cookies.getAll();

  // Set cookies
  const response = NextResponse.next();
  response.cookies.set('visited', 'true', {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 365,
  });

  // Delete cookies
  response.cookies.delete('old-cookie');

  return response;
}

Edge Runtime limitations

Middleware runs in the Edge Runtime. You cannot use:

Not available Alternative
fs module Fetch from API or use edge-compatible storage
Node.js crypto Web Crypto API (crypto.subtle)
Native modules Pure JavaScript alternatives
Most database drivers Edge-compatible clients
// Use Web Crypto instead of Node crypto
const encoder = new TextEncoder();
const data = encoder.encode('hello');
const hash = await crypto.subtle.digest('SHA-256', data);

Performance tips

  1. Keep middleware fast - it runs on every matched request
  2. Use matcher - avoid running on static assets
  3. Avoid heavy computation - move to route handlers
  4. Cache when possible - use cookies for persistent state
  5. Return early - check conditions and return quickly
export function middleware(request: NextRequest) {
  // Return early for static paths
  if (request.nextUrl.pathname.startsWith('/public')) {
    return NextResponse.next();
  }

  // Only run expensive logic when needed
  // ...
}

Summary

Next.js middleware provides:

  1. Edge execution for low latency
  2. Request interception before routes
  3. Redirects and rewrites for routing logic
  4. Auth checks without page rendering
  5. Header manipulation for security and tracking

Limitations:

  • Edge Runtime only (no Node.js APIs)
  • Runs on every matched request
  • Keep logic fast and simple

The Next.js middleware docs have the complete API reference.

Frequently Asked Questions

Middleware is code that runs before a request reaches your pages or API routes. It runs at the edge for low latency and can redirect, rewrite, or modify requests.

Common uses include authentication checks, redirects, URL rewrites, A/B testing, localization, and adding headers. Middleware runs before the response is generated.

By default yes, but you can use the matcher config to limit which paths run middleware. Always exclude static assets and Next.js internals.

Middleware runs in the Edge Runtime, so Node.js APIs like fs are not available. Use Web Standard APIs. Keep middleware fast since it runs on every matched request.

Create middleware.ts (or .js) at the root of your project, next to the app or pages directory. There can only be one middleware file.

Related Posts