Protect your Next.js API routes with middleware
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.
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.
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:
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.
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:
Run yarn dev
to start the application, if we test the application using an HTTP client, we realize we can access all the routes.
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...
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.
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.