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.
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
).
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.
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:
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:
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:
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
.
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.
1export const config = {
2 matcher: ["/users/:path*", "/dashboard/:path*"],
3};
The following routes will be matched:
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:
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.
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 isprefetch
.
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:
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:
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:
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.
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
1export function loggingMiddleware(request) {
2 console.log(`[INFO] Request to: ${request.url}`);
3}
middlewares/auth.js
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
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};