Protect your API routes in Next.js with Middleware

Protect your API routes in Next.js with Middleware
Photo by James Sutton / Unsplash

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 business logic 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 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

The middleware is executed before hitting the route's logic. The project can only file in the root directory. Inside this file, we must write do two things:

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

The middleware filename must be called, middleware followed by the extension.

Create the file 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*']
};
next-api-middleware/middleware.ts

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 on. 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 that starts with /api/users. It will match the routes like /api/users/123, /api/users/profile, etc...

Note: The matcher values need to be constants so they can be statically analyzed at build-time. Dynamic values such as variables will be ignored.

Restrict the user's routes

Update the file 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*']
};
next-api-middleware/middleware.ts

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 match, we forward the request to the next middleware if there is one or to the route directly if there is no middleware.

Restrict the admin routes

Update the file 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*']
};
next-api-middleware/middleware.ts

The code of the API route /api/auth/unauthorized is straightforward:

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.' });
};
next-api-middleware/pages/api/auth/unauthorized.ts

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

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.

Learn more about Next.js middleware here: https://nextjs.org/docs/advanced-features/middleware

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.

Happy to see you soon ?