Protect your Next.js API routes with middleware

Photo by James Sutton / Unsplash
Photo by James Sutton / Unsplash

Next.js middleware is an interesting feature that allows developers to define custom logic executed before every request is sent to their application. The middleware API is deployed as Next.js API routes on the Edge Network.

The Next.js API middleware is a function that sits in front of your application routes. It is executed before the request reaches the business logic associated with the Next.js API route.

The illustration of the API middleware in a Next.js application.
The illustration of the API middleware in a Next.js application.

There are many use cases for the Next.js middleware, which we can enumerate the following:

  • Geolocate a user based on IP address and decide what content to serve.
  • Implement API rate limiting to prevent abuse of usage of your API.
  • Detects the user language and redirects to the appropriate page.
  • Restrict access to protected API routes.

Check out the page below to view more use cases for Edge functions on Vercel.

Find your Template – Vercel
Jumpstart your app development process with pre-built solutions from Vercel and our community.

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

The use case

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

The API routes security definition.
The API routes security definition.

You can handle this case with a library like Next Auth, but one concern is that you must add the authorization check on each route. With the Next.js API middleware, you will do it in one place, and it works for all the routes.

To focus on middleware implementation, we will not implement the token generation and will pass the plain user role to the request header "Authorization".

Prerequisites

To follow this tutorial, make sure you have the following tools installed on your computer.

  • Node.js 20.6 or higher - Download link
  • NPM or Yarn (I will use Yarn)
  • An HTTP client for consuming the API; I will use Postman.

Setup the project

We will use the Next.js starter project to create a project with TypeScript. The starter project will install the latest stable version of Next.js, which is 14 at the moment I'm writing this.

Run the command below to create a Next.js project


npx create-next-app@latest --ts next-api-middleware

The CLI will ask questions to configure the application for you. The picture below shows what your project setup will look like.

Create a Next.js project with starter CLI.
Create a Next.js project with starter CLI.

Once the project is created, enter the directory and start the Next.js application.


cd next-api-middleware

yarn dev

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

Create Next.js API routes

In Next.js, all the API routes must lie in a subfolder "api" in the "app" directory; let's create one.


mkdir src/app/api

The first API route is related to authentication, which returns a token when credentials are valid. Create a file "login/route.ts" and add the code below:


import type { NextApiRequest } from 'next';

export function POST(req: NextApiRequest) {
  const { email, password } = req.body;

  console.log({ email, password });

  // TODO: authenticate user

  return Response.json({
    token: Math.random().toString(),
    expiresIn: 3600
  });
}

  • /api/contact/route.ts

import type { NextApiRequest } from 'next';

export function POST(req: NextApiRequest) {
  const { email, message } = req.body;

  console.log({ email, message });

  // TODO send email

  return Response.json({ message: "The request processed successfully!" });
}

  • /api/users/create/route.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import { User, users } from "@/data/users";

export function POST(req: NextApiRequest) {
  const userToAdd: User = {
    id: Math.random().toString(),
    name: req.body.name,
    role: "user",
  };

  users.push(userToAdd);

  return Response.json({ ...userToAdd });
}


  • /api/users/list/route.ts

import { User, users } from "@/data/users";

export function GET() {
  const simpleUsers: User[] = users.slice().filter((user) => user.role === "user");

  return Response.json({ data: simpleUsers });
}


  • /api/admin/list/route.ts

import { User, users } from "@/data/users";

type Data = {
  data: User[];
}

export function GET() {
  const simpleUsers: User[] = users.filter((user) => user.role === "admin");

  return Response.json({ data: simpleUsers });
}

Finally, the file that contains the list of users in "src/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 };

The folder structure now looks like this:

The project folder structure with the API routes.
The project folder structure with API routes.Next.js

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 Next.js API routes

The middleware is executed before the route's logic is reached. The project can have only one middleware file at the same level as the "app" directory.

  • If your Next.js project has the "src" folder, the middleware file location is "src/middleware.ts"; otherwise, it is "./middleware.ts".

Inside the next.js middleware file, we must define two things:

  • The routes where this middleware will apply. As you can guess, all the routes under /api/users and /api/admin.
  • An action to execute before continuing the request execution.

Create the file "src/middleware.ts" and add the code below:


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

export async function middleware(req: NextRequest) {
  

  return NextResponse.next();
}

export const config = {
    matcher: ['/api/users/:path*', '/api/admin/:path*']
};

The middleware just forwards the request to the route, but the interesting part is the config variable.

The properties matcher is an array of routes to apply the middleware. It supports the wildcard syntax, so you can match a group of routes.

The syntax /api/users/:path* says: Apply the middleware on all the routes starting with /api/users. It will match the routes like /api/users/123, /api/users/profile, etc...

💡
In the config object, the values of the property "matcher" need to be constant to be statically analyzed at build time. Dynamic values such as variables will be ignored.

Restrict the user's routes

Update the file "src/middleware.ts" with the code below:


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

const isUserRoute = (pathname: string) => {
    return pathname.startsWith('/api/users');
}

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

  if (isUserRoute(pathname) && !includes(["user", "admin"], role)) {
    return NextResponse.redirect(new URL('/api/auth/unauthorized', req.url));
  }

  return NextResponse.next();
}

export const config = {
    matcher: ['/api/users/:path*', '/api/admin/:path*']
};

We retrieve the authorization value from the request header, and if it doesn't match, we redirect the user to the unauthorized route that will return a 401 status code.

If the value matches, we forward the request to the next middleware if there is one or to the route directly otherwise.

Restrict the admin routes

Update the file "src/middleware.ts" with the code below:


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

const isAdminRoute = (pathname: string) => {
    return pathname.startsWith('/api/admin');
}

const isUserRoute = (pathname: string) => {
    return pathname.startsWith('/api/users');
}

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

  if (isUserRoute(pathname) && !includes(["user", "admin"], role)) {
    return NextResponse.redirect(new URL('/api/auth/unauthorized', req.url));
  }

  if (isAdminRoute(pathname) && role !== "admin") {
    return NextResponse.redirect(new URL('/api/auth/unauthorized', req.url));
  }

  return NextResponse.next();
}

export const config = {
    matcher: ['/api/users/:path*', '/api/admin/:path*']
};

The code of the API route /api/auth/unauthorized is straightforward; create a file "auth/unauthorized/route.ts" and add the code below.


import type { NextApiRequest, NextApiResponse } from 'next';

type ResponseBody = { message: string };

export default function handler(req: NextApiRequest, res: NextApiResponse<ResponseBody>) {
  res.status(401).json({ message: 'Not authenticated.' });
};

Re-run the application with the command yarn dev and call the API routes again.

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

The API routes of the Next.js API are now protected.

Limit of using Next.js API middleware

The middleware is deployed in the Edge Network and has some limitations:

  • The code runs on Edge runtime, in which not all the Node.js APIs are available; you must ensure your middleware doesn't use an API that is not supported; check out the supported Node.js APIs.
  • The bundle size (code, libraries, and assets) is limited to 1MB; you can increase this limit on the paid plan. If the bundle size exceeds this limit, you will get an error during the production build of your Next.js application.
  • Other limitations are memory, execution duration, environment variables, etc.

Wrap up

The Next.js API middleware helps perform some actions before the request hits your API routes. A good middleware implementation can help improve the user experience, content delivery, and access control.

Also, the limitations of using the middleware, such as the bundle size and the Node.js module compatibility, should be considered.

To go further, check the Next.js API middleware documentation to see what you can do with it.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.