Building Secure Middleware for Both API and Client Routes in Next js

Samir Mirzaliyev
3 min readSep 27, 2024

Hey everyone, I’m back after a long break. As we all know, handling server and client requests efficiently is crucial when building modern web applications. In a Next.js app, middleware can act as a bridge between the incoming request and the response. It allows you to handle authentication, route redirection, and token validation before proceeding to the next step. Today, we’re going to explore how to implement middleware for both the server and client in Next.js apps. Let’s dive in

Next.js enables us to create this file in the project’s root directory. It only supports one middleware file per project, so to maintain a simple code structure, we should separate functionalities in this file.

export default async function middleware(req: NextRequest) {
const isApiRoute = req.nextUrl.pathname.startsWith(apiPrefix);

if (isApiRoute) {
return await apiMiddleware(req);
}

return await clientMiddleware(req);
}

export const config = {
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

First, the matcher option ensures that middleware runs on every route except static assets and internal Next.js paths like _next. Then, the middleware function first checks whether the request is for an API route by checking the URL prefix.

export async function apiMiddleware(req: NextRequest) {
const { nextUrl, headers } = req;

if (nextUrl.pathname.startsWith(apiAuthPrefix)) {
return;
}

const accessToken = getAccessToken(headers);

if (!accessToken) {
return NextResponse.json({ message: "Token is missing"}, {status: 401});
}

const hasVerifiedAccessToken = await tokenService.verifyAndDecodeToken(
accessToken,
TokenType.ACCESS
);

if (!hasVerifiedAccessToken) {
return NextResponse.json({ message: "Token is expired"}, {status: 401});
}

return;
}

The main logic of the api-middleware function is quite simple. First, it checks the URL to see if it’s an authentication route, such as a login or registration route. If it is, there’s no need to check it further; we can just let it go.

So, if not, we will just take a token from the request’s header and verify it. If it’s expired, we will break the request and send an expired message with a new response.

export async function clientMiddleware(req: NextRequest) {
const { nextUrl, cookies, url } = req;

const refreshToken = cookies.get(REFRESH_TOKEN_NAME)?.value;

const hasVerifiedRefreshToken = refreshToken
? await tokenService.verifyAndDecodeToken(refreshToken, TokenType.REFRESH)
: false;

const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
const isAuthRoute = authRoutes.includes(nextUrl.pathname);

if (isAuthRoute) {
return handleAuthRoute(!!hasVerifiedRefreshToken, url);
}

if (!hasVerifiedRefreshToken && !isPublicRoute) {
return handleUnauthorizedAccess(nextUrl, url);
}

return;
}

In the client middleware, we check and verify the refresh token from the request’s cookies. This allows us to determine if the current route is public, an authentication route, or a protected route. If the user is on an authentication route (e.g., /login) and already has a valid token, they will be redirected to the dashboard. However, if a user is trying to access a protected route without a valid token, they will be redirected to the login page.

function handleAuthRoute(hasVerifiedRefreshToken: boolean, url: string) {
if (hasVerifiedRefreshToken) {
return NextResponse.redirect(new URL("/dashboard", url));
} else {
const response = NextResponse.next();
response.cookies.delete(REFRESH_TOKEN_NAME);
return response;
}
}

function handleUnauthorizedAccess(nextUrl: URL, url: string) {
const response = NextResponse.redirect(
new URL("/login", url)
);
response.cookies.delete(REFRESH_TOKEN_NAME);
return response;
}

The handleAuthRoute function redirects authenticated users from the login or signup pages if they’re already logged in.

The handleUnauthorizedAccess function redirects unauthenticated users attempting to access protected routes back to the login page and removes the invalid token from the request’s cookies.

To effectively manage requests in your Next.js app, you can divide the middleware logic between API and client routes. The API middleware handles token validation for server-side routes, while the client middleware deals with managing authentication and routing for user-facing pages.

That’s all for now. I hope this article helps you understand the logic of middleware. Goodbye, and I’ll see you in the next article!

--

--

Samir Mirzaliyev
Samir Mirzaliyev

No responses yet