Next.js Middleware: Simple Guide to Control Requests

harshalranjhani

Harshal Ranjhani

Posted on September 4, 2024

Next.js Middleware: Simple Guide to Control Requests

Middleware is a powerful concept in web development that allows you to run code before or after a request is processed. In this article, we will learn how to use middleware in Next.js, exploring the implementation of Next.js middleware and its capabilities.

What is Middleware in Next.js?

Middleware in Next.js is a function that runs before a request is completed. It provides a way to process requests before they reach the final destination within your application, allowing you to modify the request, perform certain actions, or even redirect users based on specific conditions.

If you’re familiar with Express.js or similar Node.js frameworks, you might already have a decent idea of how middleware functions work. In a way, Next.js middleware works similarly but is integrated with the Next.js ecosystem, making it easier to work within the context of a full-featured, production-ready React framework.

Use Cases for Next.js Middleware

Integrating middleware in your Next.js application can be beneficial in various scenarios. Here are some common use cases where you might want to use middleware:

  • Authentication: You can use middleware to check if a user is authenticated before allowing them to access certain routes.

  • Logging: Middleware can be used to log requests, responses, or other information related to the application.

  • Error Handling: You can create middleware to handle errors that occur during the request processing.

  • Caching: Middleware can be used to cache responses or data to improve performance.

  • Request Processing: You can modify or process requests before they reach the final destination.

  • Bot Detection: Middleware can be used to detect and block bots or malicious requests.

Situations Where Middleware is Not Recommended

While middleware can be a powerful tool, there are situations where it might not be the best choice. Here are some scenarios where you might want to avoid using middleware:

  • Heavy Processing: If your middleware performs heavy processing, it can slow down the request processing time.

  • Complex data fetching: If your middleware needs to fetch data from external sources or perform complex operations, it can introduce latency.

  • Direct Database Access: Avoid accessing the database directly from middleware, as it can lead to security vulnerabilities.

Block Diagram of Next.js Middleware

Here is an example block diagram of how middleware works in Next.js. This diagram only shows the basic flow of middleware in Next.js and does not cover all possible scenarios.

Middleware in Next.js

The NextResponse Object

The NextResponse object is central to what you can do within your middleware. Here are a few things you can accomplish with it:

  • Respond to Requests: You can return a specific response such as a JSON object or redirection.

  • Modify the Response: Adjust headers, cookies, or the response method entirely based on specific conditions.

  • Rewrite the Request Path: This is handy when you want to serve the content of one path in response to another request path without an outright redirection.



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

export function middleware(req: NextRequest) {
  if (req.url === '/old-route') {
    return NextResponse.rewrite(new URL('/new-route', req.url));
  }

  return NextResponse.next();
}


Enter fullscreen mode Exit fullscreen mode

In this example, any requests to /old-route will be internally rewritten to /new-routewithout the user being redirected.

Convention for Next.js Middleware

We use the middleware.ts file to define our middleware functions. This file should be placed in the root of your Next.js project.



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

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/home", request.url));
}

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


Enter fullscreen mode Exit fullscreen mode

In the above example, we define a middleware function that redirects the user to the /home route if they try to access the /about route. We also specify a matcher to define the route pattern that the middleware should apply to.

Matching Paths

Middleware will be invoked for every route in the project. Because of this, it is important to specify a matcher to define the routes that the middleware should apply to. The matcher can be a string or a regular expression that matches the route pattern.

Here are some examples of matchers:

  • /about: Matches the /about route.

  • /blog/:slug: Matches any route that starts with /blog/ followed by a slug.

  • /api/*: Matches any route that starts with /api/.

  • /([a-zA-Z0-9-_]+): Matches any route that consists of alphanumeric characters, hyphens, and underscores.

Matching Multiple Paths

You can also specify multiple matchers by using an array of strings or regular expressions. Middleware will be invoked for any route that matches any of the specified matchers.



export const config = {
  matcher: ["/about", "/blog/:slug"],
};


Enter fullscreen mode Exit fullscreen mode


// Regular expression matcher
export const config = {
  matcher: ["/([a-zA-Z0-9-_]+)"],
};


Enter fullscreen mode Exit fullscreen mode

You can read more about path-to-regexp syntax here.

Bypassing Next.js Middleware

You can also bypass Middleware for certain requests by using the missing or has arrays, or a combination of both:



export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    {
      source:
        "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },

    {
      source:
        "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
      has: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },

    {
      source:
        "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
      has: [{ type: "header", key: "x-present" }],
      missing: [{ type: "header", key: "x-missing", value: "prefetch" }],
    },
  ],
};


Enter fullscreen mode Exit fullscreen mode

Conditional Statements in Next.js Middleware

You can use conditional statements in your middleware functions to perform different actions based on specific conditions. Here is an example of how you can use conditional statements in Next.js middleware:



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

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}


Enter fullscreen mode Exit fullscreen mode

In the above example, we use conditional statements to check if the request path starts with /about or /dashboard. If the condition is met, we rewrite the URL to a different path.

Detailed Use Cases for Next.js Middleware

Routing

Routing control enables you to redirect users to different pages based on specific conditions. You can use middleware to check if a user is authenticated and redirect them to the login page if they are not.



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

export function middleware(req: NextRequest) {
  const token = req.cookies['auth-token'];

  if (!token) {
    // Redirect to login page if not authenticated
    return NextResponse.redirect('/login');
  }

  // Allow the request to proceed
  return NextResponse.next();
}


Enter fullscreen mode Exit fullscreen mode

Logging and Analytics

Middleware can be used to log requests, responses, or other information related to the application. You can use middleware to log information such as request headers, response status codes, and more.



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

export function middleware(req: NextRequest) {
  console.log('Accessed Path:', req.url);
  console.log('User Agent:', req.headers.get('user-agent'));

  return NextResponse.next();
}


Enter fullscreen mode Exit fullscreen mode

Here, we log the accessed path and user agent for each request.

Geolocation-Based Content Rendering

You can use middleware to detect the user's geolocation and serve content based on their location. For example, you can redirect users to a specific page based on their country or region.



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

export function middleware(req: NextRequest) {
  const country = req.headers.get('geo-country');

  if (country === 'US') {
    return NextResponse.rewrite(new URL('/us-homepage', req.url));
  }

  return NextResponse.next();
}


Enter fullscreen mode Exit fullscreen mode

In this example, users from the US will be redirected to the /us-homepage route. You can customize the behavior based on different geolocations.

Preventing Bot Activity and Rate Limiting

Middleware is suitable for checking the legitimacy of a request, like rate limiting or identifying bot traffic. You can adjust the response for bots (e.g., showing a CAPTCHA) or limit the rate of requests coming from a particular IP address to prevent DDoS attacks.



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

interface RateLimitRecord {
  lastRequestTime: number;
  requestCount: number;
}

// In-memory store for request rates
const rateLimitStore: Map<string, RateLimitRecord> = new Map();

const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute window
const RATE_LIMIT_MAX_REQUESTS = 5; // Max 5 requests per window

export function middleware(req: NextRequest) {
  const userAgent = req.headers.get('user-agent')?.toLowerCase();
  const isBot = userAgent?.includes('bot') ?? false;

  // Prevent bot activity by routing bots to a special detection page
  if (isBot) {
    return NextResponse.rewrite(new URL('/bot-detection', req.url));
  }

  // Get client IP address
  const clientIp = req.ip ?? 'unknown';

  // Initialize or update the rate limit record for this IP
  const currentTime = Date.now();
  const rateLimitRecord = rateLimitStore.get(clientIp);

  if (rateLimitRecord) {
    // Check if the current request is within the rate limit window
    const elapsedTime = currentTime - rateLimitRecord.lastRequestTime;

    if (elapsedTime < RATE_LIMIT_WINDOW_MS) {
      // Within the same window, increment the request count
      rateLimitRecord.requestCount += 1;

      if (rateLimitRecord.requestCount > RATE_LIMIT_MAX_REQUESTS) {
        // Rate limit exceeded, deny request
        return new NextResponse(
          JSON.stringify({ error: `Too many requests. Please try again later.` }),
          { status: 429, headers: { 'Content-Type': 'application/json' } }
        );
      }
    } else {
      // Reset the window and request count
      rateLimitRecord.lastRequestTime = currentTime;
      rateLimitRecord.requestCount = 1;
    }
  } else {
    // Create a new rate limit record for this IP
    rateLimitStore.set(clientIp, {
      lastRequestTime: currentTime,
      requestCount: 1,
    });
  }

  // Allow the request to proceed
  return NextResponse.next();
}


Enter fullscreen mode Exit fullscreen mode

In this example, we prevent bot activity by redirecting bots to a special detection page. We also implement rate limiting to restrict the number of requests coming from a specific IP address within a given time window. This helps prevent DDoS attacks and ensures fair usage of server resources.

Best Practices

When using middleware, it's essential to be mindful of your application’s overall performance and security:

  • Minimize middleware complexity: Keep middleware functions lightweight, aiming to reduce latency introduced by each request.

  • Scope properly: Only apply middleware where necessary. Overuse or incorrectly scoped middleware can lead to unnecessary complications.

  • Use middleware for generic logic: Middleware is best used for shared logic that applies across multiple routes.

  • Combine multiple pieces of middleware: If your middleware becomes too complex, consider splitting it up and chaining multiple middleware functions.

Conclusion

Middleware in Next.js is a powerful tool that allows you to process requests before they reach the final destination. By using middleware, you can implement various features such as authentication, logging, error handling, and more. Understanding how to use middleware effectively can help you build robust and secure applications with Next.js. Try implementing middleware in your Next.js project to enhance its functionality and improve user experience.

You can read more about the Next.js middleware in the official documentation here.

Happy coding! 🚀

💖 💪 🙅 🚩
harshalranjhani
Harshal Ranjhani

Posted on September 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related