Photo by James Sutton / Unsplash

Next.js 12 had significant features and improvements like the build times 5x faster; another exciting feature released called Edge Network allows you to deploy your functions in the Edge and increase the execution speed. An extension of this feature is the support of middleware in a Next.js application. A middleware is a function that sits in front of your application routes. It is executed before the request reaches the logic route associated with a route.

With middleware, you can do many things like:

  • Geolocate a user based on IP address
  • Implement API rate limiting
  • Detects the user language and redirects to the appropriate page.
  • Restrict access to specific routes

On this page, you can find other use cases for Edge functions on Vercel

Edge Functions – Vercel
Vercel Edge Functions are the benefits of static with the power of dynamic.

In this post, we will see how to protect our API routes using the middleware.

The use case

The picture below shows the available API routes on a Next.js application with the restriction we want to apply:

API routes security definition
API routes security definition

With a library like Next Auth, you can handle this case, but one of the concerns is that you will have to add the authorization check on each route. With the middleware, you will do it in one place, and it works for all the routes.

We will not implement the token generation, and we will pass the plain user role to the request header "Authorization".

Setup the project

We will use the Next.js starter project to create a project with typescript. Make sure to install a version of Next.js >= 12.

Run the commands below to install the project and start the application:

npx create-next-app@latest --ts next-api-middleware
cd next-api-middleware
yarn dev

Navigate to http://localhost:3000 to make sure the application works as expected.

Now we will create our API routes:

  • /api/login.ts
import type { NextApiRequest, NextApiResponse } from 'next';

type Data = {
  token: string;
  expiresIn: number;
}

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  res.status(200).json({ token: Math.random().toFixed(), expiresIn: 3600 });
};
next-api-middleware/pages/api/login.ts
  • /api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next';

type Data = {
  message: string;
}

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  // TODO send email

  res.status(200).json({ message: "The request processed successfully!" });
};
next-api-middleware/pages/api/contact.ts
  • /api/users/create.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User, users } from "../../../data/users";

export default function handler(req: NextApiRequest, res: NextApiResponse<User>) {
  const userToAdd: User = {
    id: Math.random().toString(),
    name: req.body.name,
    role: "user",
  };

  users.push(userToAdd);

  res.status(200).json({ ...userToAdd });
};
next-api-middleware/pages/api/users/create.ts
  • /api/users/list.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { filter } from "lodash";
import { User, users } from "../../../data/users";

type Data = {
  data: User[];
}

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  const simpleUsers: User[] = filter(users, (user) => user.role === "user");

  res.status(200).json({ data: simpleUsers });
};
next-api-middleware/pages/api/users/list
  • /api/admin/list.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { filter } from "lodash";
import { User, users } from "../../../data/users";

type Data = {
  data: User[];
}

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  const simpleUsers: User[] = filter(users, (user) => user.role === "admin");

  res.status(200).json({ data: simpleUsers });
};
next-api-middleware/pages/api/admin/list.ts

Finally, the file that contains the list of users in /data/users.ts

export type User = {
  id: string;
  name: string;
  role: "user" | "admin";
}

const users: User[] = [
  { id: "111", name: "User 1", role: "user" },
  { id: "112", name: "User 2", role: "user" },
  { id: "113", name: "User 3", role: "admin" },
];

export { users };
next-api-middleware/pages/data/users.ts

The folder structure now looks like this:

Project folder structure with all the routes created
Project folder structure with all the routes created

Run yarn dev to start the application, if we test the application using an HTTP client, we realize we can access all the routes.

Test the API endpoints with Postman
Test the API endpoints with Postman

Let's use the middleware to protect the routes.

Protect API routes

One of the particularities of the middleware compared to those in other frameworks like Express is that it is also route-based so, if you create a middleware inside a folder, it applies only to the routes inside this folder. Knowing that we will create two middleware:

  • /api/users: check if the authenticated user has the role "user".
  • /api/admin: check if the authenticated user has the role "admin".

The middleware name should start with an underscore _ then followed by the word middleware; nothing more which give us _middleware.

Create the middleware for users routes

Inside the folder pages/api/users, create a file _middleware.ts and then, the code below:

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

export async function middleware(req: NextRequest) {
  const role = req.headers.get("authorization");

  if (["user", "admin"].includes(role)) {
    return new Response(JSON.stringify({ message: 'Not authenticated.' }), {
      status: 401,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  return NextResponse.next();
}

Here, we retrieve the authorization value from the request header, and if it doesn't match, we return a 401 HTTP status code; otherwise, we forward the request to the next middleware if there is one or to the route directly if there is no middleware.

Create the middleware for admin routes

The code isn't so different from the preview middleware. Inside the folder pages/api/admin, create a file _middleware.ts and then, the code below:

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

export async function middleware(req: NextRequest) {
  const role = req.headers.get("authorization");

  if (role !== "admin") {
    return new Response(JSON.stringify({ message: 'Not authenticated.' }), {
      status: 401,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  return NextResponse.next();
}

Now, start the application yarn dev and test again:

Test the protected endpoints to see the behavior
Test the protected endpoints to see the behavior

Test the API endpoints with Postman

Now our endpoints are protected 🎉

Beware of your middleware size

At the moment I'm writing this, the middleware size is limited to 1MB. When building the production application, you will get an error if the build size exceeds.

Wrap up

It is the end of this post, and we can summarize that the middleware provides a great way to handle actions that applies to a group of routes. It can also be used on the frontend side. It is still in beta but will probably be released soon.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to not miss the upcoming posts and the tips and tricks I share every week.

Happy to see you soon 😉