Middleware in Next.js allows developers to run code before a request reaches a page or route handler. It acts like a request filter that sits between the user request and the application, providing a powerful way to handle cross-cutting concerns in your application.

Middleware executes in an Edge Runtime environment, making it extremely fast and ideal for performance-sensitive operations. It can be used for various purposes:

  • Authentication & Authorization: Verify user identity and permissions before accessing protected resources
  • Route Protection: Restrict access to specific routes based on user roles or credentials
  • Intelligent Redirects: Route users to appropriate pages based on location, device, or other criteria
  • Request Logging: Log all incoming requests for monitoring and debugging
  • A/B Testing: Route users to different versions of your application
  • Localization: Detect user locale and serve localized content
  • Security Headers: Add security headers to protect against common vulnerabilities
  • Rate Limiting: Control request rate to prevent abuse

Middleware runs before rendering happens and operates on the Edge, which makes it extremely powerful and performant compared to traditional server-side middleware.


How middleware works

Middleware in Next.js is a function that runs before a request reaches a page or route handler. It acts like a request filter that sits between the user request and the application.

When a request is received, it first goes through the middleware functions before reaching the final destination.

User Request → Middleware → Next.js Route / Page → Response

What middleware does

  • Allow the request
  • Modify the request
  • Redirect the request
  • Block the request

Where middleware is defined

Middleware is defined in a file named `middleware.ts` or `middleware.js` at the root of the project or inside the `app` directory.


  project/
        ├ app/
        ├ middleware.ts
        ├ package.json
  

The file name must be `middleware.ts` or `middleware.js`. Next JS will automatically detect and use the middleware file.


Basic Middleware Example

Here is a simple example of a middleware function:


import { NextResponse } from "next/server"
import { NextRequest } from "next/server"

export function middleware(request: NextRequest) {

  console.log("Middleware executed")

  return NextResponse.next()
}

// Only run this middleware for /about (and any nested routes).
// export const config = { matcher: ["/about/:path*"] };

In the example above, the middleware function logs a message to the console and then calls `NextResponse.next()` to continue processing the request.

This will run every time a request is received.

export const config = { matcher: ["/about/:path*"] }; will only run the middleware for the /about route and any nested routes.


NextRequest

The `NextRequest` object contains information about the incoming request, such as headers, URL, and query parameters.

It is passed as an argument to the middleware function. The name of the parameter is `request` or `req`.


export function middleware(request: NextRequest) {

  const url = request.nextUrl.pathname

  console.log(url)

  return NextResponse.next()
}

What nextRequest can access

  • URL path
  • Headers
  • Cookies
  • Query Parameters


Protecting Routes with Middleware

You can use middleware to protect routes by checking if the user is authenticated before allowing access to certain pages. For example:

 
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {

  const token = request.cookies.get("token");

  if (!token) {

    return NextResponse.redirect(
      new URL("/login", request.url)
    )
  }

  return NextResponse.next()
}

If the token does not exist, the user is redirected to the login page.


Running Middleware for Specific Routes

We can specify which routes the middleware should run on using the `config` object. For example


export const config = {
  matcher: "/dashboard/:path*",
}

Multiple Route Matchers

You can specify multiple routes for the middleware to run on by providing an array of matchers.


export const config = {
  matcher: ["/dashboard/:path*", "/profile/:path*"],
}

Reading Cookies

You can read cookies from the incoming request using the `cookies` property of the `NextRequest` object.


export function middleware(request: NextRequest) {

  const token = request.cookies.get("token");

  console.log(token)

  return NextResponse.next()
}

The `cookies.get()` method returns the value of the specified cookie, or `undefined` if the cookie does not exist.


Modify Response Headers

You can modify the response headers using the `NextResponse` object. For example, to add a custom header:


export function middleware(request: NextRequest) {

  const response = NextResponse.next()

  response.headers.set("x-custom-header", "hello")

  return response
}

In the example above, we create a response object using `NextResponse.next()`, then we set a custom header `x-custom-header` with the value `hello`, and finally we return the modified response.


Real-World Middleware Use Cases

1. Authentication & Token Verification

Verify JWT tokens or session cookies before allowing access to protected routes.


export function middleware(request: NextRequest) {
  const token = request.cookies.get("auth_token")?.value;
  
  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  
  return NextResponse.next();
}

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

2. Rate Limiting

Limit request frequency to prevent abuse and DDoS attacks.


const requestCounts = new Map();
const RATE_LIMIT = 100; // requests
const TIME_WINDOW = 60000; // 1 minute

export function middleware(request: NextRequest) {
  const ip = request.ip || "unknown";
  const now = Date.now();
  
  if (!requestCounts.has(ip)) {
    requestCounts.set(ip, []);
  }
  
  const timestamps = requestCounts.get(ip) || [];
  const recentRequests = timestamps.filter(
    (t) => now - t < TIME_WINDOW
  );
  
  if (recentRequests.length >= RATE_LIMIT) {
    return NextResponse.json(
      { error: "Too many requests" },
      { status: 429 }
    );
  }
  
  recentRequests.push(now);
  requestCounts.set(ip, recentRequests);
  
  return NextResponse.next();
}

3. Localization & Language Detection

Route users to localized versions of your site based on their language preference.


export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  
  // Check if locale is already in pathname
  const locales = ["en", "es", "fr", "de"];
  const hasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );
  
  if (hasLocale) return NextResponse.next();
  
  // Get user's preferred language from Accept-Language header
  const preferredLocale = 
    request.headers.get("accept-language")?.split(",")[0].split("-")[0] || "en";
  
  return NextResponse.redirect(
    new URL(`/${preferredLocale}${pathname}`, request.url)
  );
}

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

4. Security Headers

Add security headers to protect against common web vulnerabilities.


export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // Prevent clickjacking
  response.headers.set("X-Frame-Options", "DENY");
  
  // Prevent MIME type sniffing
  response.headers.set("X-Content-Type-Options", "nosniff");
  
  // Enable XSS protection
  response.headers.set("X-XSS-Protection", "1; mode=block");
  
  // Content Security Policy
  response.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  );
  
  return response;
}

5. A/B Testing

Route users to different versions for experimentation and testing.


export function middleware(request: NextRequest) {
  const ab_test = request.cookies.get("ab_test")?.value || "control";
  
  if (request.nextUrl.pathname === "/features/new") {
    if (ab_test === "variant") {
      request.nextUrl.pathname = "/features/new-variant";
    }
  }
  
  return NextResponse.rewrite(request.nextUrl);
}

Middleware Best Practices

1. Keep Middleware Fast

  • Middleware runs on the Edge, so minimize processing time
  • Avoid heavy computations or database queries
  • Cache authentication tokens or session data when possible

2. Use Matcher Wisely

  • Specify matcher patterns to run middleware only on necessary routes
  • Running on all routes increases latency for external requests
  • Exclude static assets and API routes that don't need middleware

export const config = {
  matcher: [
    // Run on dashboard routes
    "/dashboard/:path*",
    // Run on API routes requiring auth
    "/api/auth/:path*",
    // Exclude specific paths
    "/((?!public|_next/static|_next/image|favicon.ico).*)",
  ],
};

3. Handle Errors Gracefully

  • Always return a valid Response object
  • Provide meaningful error messages
  • Avoid exposing sensitive information in error responses

4. Log Middleware Activity

Implement logging to monitor middleware behavior and debug issues.


export function middleware(request: NextRequest) {
  const start = Date.now();
  const response = NextResponse.next();
  const duration = Date.now() - start;
  
  console.log({
    path: request.nextUrl.pathname,
    method: request.method,
    duration: `${duration}ms`,
    timestamp: new Date().toISOString(),
  });
  
  return response;
}

5. Use TypeScript for Type Safety

  • Use Next.js types for `NextRequest` and `NextResponse`
  • Define interfaces for custom request/response properties
  • Catch type errors at development time

Common Pitfalls to Avoid

1. Running Expensive Operations

✗ Don't: Make database calls or complex computations in middleware


// AVOID: This is too slow
export async function middleware(request: NextRequest) {
  const user = await db.query("SELECT * FROM users..."); // Too slow!
  return NextResponse.next();
}

✓ Do: Cache data or use lightweight checks


// BETTER: Use cached token validation
export function middleware(request: NextRequest) {
  const token = request.cookies.get("auth_token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

2. Ignoring Matcher Performance

Middleware without matcher patterns runs on every request, including static assets. Always specify matchers for critical routes only.

3. Not Handling Redirects Properly

Using `NextResponse.redirect()` multiple times can create redirect loops. Test redirect chains carefully.

4. Exposing Sensitive Information

Never log or expose API keys, passwords, or user data in middleware logs or error responses.

5. Blocking Valid Requests

Be careful with authentication checks - ensure valid tokens are properly recognized and don't accidentally block legitimate users.