Middleware

Middleware is a piece of code that executes after the server receives a request, but before it is processed and the response is sent. It is something that happens in the middle, hence the name middleware.

Middlewares are commonly used for user authentication, logging, traffic control, bot detection, and more.

In this lesson, we are going to explain how to create middlewares in Next.js applications, what you can do inside a middleware, and how to configure the middleware so that it will only be activated for selected routes, and how to execute different middlewares for different routes.

Creating a middleware

In Next.js, middlewares can be created by adding a middleware.js file under the src directory.

text
1src
2├── app
3│   ├── favicon.ico
4│   ├── globals.css
5│   ├── layout.jsx
6│   └── page.jsx
7├── libs
8└── middleware.js     <===== Middleware

This file should export two things, a middleware function and a configuration object (config).

javascript
1export function middleware(request) {
2  console.log(request.nextUrl.pathname);
3}
4
5export const config = {
6  matcher: "/:path*",
7};

This example middleware will be executed for every route, and when a route is visited, this middleware will log the requested path.

Logged paths

Setting up matchers

And now, we need to take a closer look at what we just created.

Let's start with the config object. This configuration object contains a matcher that determines if the middleware will be invoked for the requested route.

For example:

javascript
1export const config = {
2  matcher: "/users/:path",
3};

In this case, the middleware() will be executed whenever a request is sent to a route that follows the pattern /users/:path, where :path is a parameter that matches a route segment.

The following routes will be matched:

text
1/users/1
2/users/123
3/users/999

Matching multiple segments

But, what if there are more route segments? For example, what if you need to match routes such as:

text
1/users/1/profile
2/users/100/profile/details
3/users/123/profile/details/settings

There is a way to match for all of them at once, by appending an asterisk (*) after the :path parameter. This is a wildcard symbol allowing you to match for any number of route segments after /users.

javascript
1export const config = {
2  matcher: "/users/:path*",
3};

Matching multiple routes

The matcher option also accepts an array, allowing you to match for multiple paths.

javascript
1export const config = {
2  matcher: ["/users/:path*", "/dashboard/:path*"],
3};

The following routes will be matched:

text
1/users/1/profile
2/users/100/profile/details
3/users/123/profile/details/settings
4
5/dashboard/preference
6/dashboard/posts/create
7/dashboard/user/123/posts

Matching with regex

Now, look at the previous examples, doesn't the asterisk (*) look familiar? Yes, it is Regex.

Next.js supports full Regex expressions, so advanced matching logics such as look ahead, look behind, and others are also possible here. For example:

javascript
1export const config = {
2  matcher: "/((?!api|_next).*)",
3};

This Regex pattern matches for everything except for the ones that starts with api or _next.

You can learn more about Regex in the linked lesson.

Advanced matching options

Lastly, the matcher accepts a few advanced options that allows you to further customize the matching logic.

javascript
1export const config = {
2  matcher: [
3    {
4      source: "/users/:path*",
5      has: [{ type: "header", key: "x-present" }],
6      missing: [{ type: "header", key: "x-missing", value: "prefetch" }],
7    },
8  ],
9};

Each of these matching object has three options, source, has, and missing.

  • sources contains the actual matching pattern, just like we have discussed above.
  • has specifies the conditions that allows a route to be matched only if the condition is present.
  • missing specifies the conditions that allows a route to be matched only if the condition is NOT present.

For the above example, a route will only be matched if:

  • It starts with /users.
  • The request contains a header with key x-present.
  • The request does not contain a header with key x-missing, whose value is prefetch.

The middleware() function

Now we know how to use matchers to decide which route shall activate the middleware when visited, let's look into the middleware() function itself.

Again, middleware is a piece of code that runs between a request and a response, which means it has access to both of them. A middleware can read information from the request, and optionally return a response. This brings a lot of possibilities.

For example, you can used them for redirects:

javascript
1import { NextResponse } from "next/server";
2
3export function middleware(request) {
4  if (request.nextUrl.pathname.startsWith("/about")) {
5    return NextResponse.rewrite(new URL("/about-2", request.url));
6  }
7
8  if (request.nextUrl.pathname.startsWith("/dashboard")) {
9    return NextResponse.rewrite(new URL("/dashboard/user", request.url));
10  }
11}

User authentication and authorization:

javascript
1export function middleware(request) {
2  if (!isAuthenticated(request)) {
3    return NextResponse.redirect(new URL("/login", request.url));
4  }
5}

Or you can implement a logging system that records the request information:

javascript
1export function middleware(request) {
2  console.log(request.nextUrl.pathname);
3}

And a lot more possibilities. What you use middleware for entirely depends on you.

Modularize your middlewares

You may have noticed that there is an issue with this middleware: what if we need to execute different middleware functions for different routes.

For example, what if you need a logging middleware that runs for every route and a user auth middleware that runs for only routes that start with /dashboard?

Currently, there's no native way to do this, as Next.js only supports one middleware.js file under the src directory. But, we can still implement this feature with some smart programming.

Create a middlewares directory, and inside, we'll have our various middlewares.

text
1src
2├── app
3├── libs
4├── middleware.js    <===== Aggregated middleware
5└── middlewares
6    ├── auth.js      <===== User authentication middleware
7    └── logging.js   <===== Logging middleware

middlewares/logging.js

javascript
1export function loggingMiddleware(request) {
2  console.log(`[INFO] Request to: ${request.url}`);
3}

middlewares/auth.js

javascript
1import { NextResponse } from "next/server";
2
3export function authMiddleware(request) {
4  // Simulating user auth
5  const isAuthenticated = false;
6
7  if (isAuthenticated) {
8    console.log("User authenticated.");
9  } else {
10    console.log("User not authenticated.");
11    return NextResponse.redirect(new URL("/login", request.url));
12  }
13}

The idea is to aggregate these middleware functions into the middleware.js, and use an if block to decide which middleware function should be executed.

middleware.js

javascript
1import { loggingMiddleware } from "./middlewares/logging";
2import { authMiddleware } from "./middlewares/auth";
3
4export function middleware(request) {
5  loggingMiddleware(request);
6
7  if (request.nextUrl.pathname.startsWith("/users")) {
8    return authMiddleware(request);
9  }
10}
11
12export const config = {
13  matcher: "/:path*",
14};