Using Middleware in Next.js for Route Protection

Hero image for Using Middleware in Next.js for Route Protection. Image by Jamie Street.
Hero image for 'Using Middleware in Next.js for Route Protection.' Image by Jamie Street.

When building web applications, some pages and APIs should not be freely accessible to the general public. We need an effective way to check who's making requests, redirect unauthorised visitors, or limit access to specific routes. Next.js's middleware provides a clean, central place to handle this logic, making route protection simple and easy to maintain.

In this article, I intend to explain what Next.js middleware does, how we can use it to protect our routes, and share some practical examples, including how I recently used middleware to protect sensitive APIs in a browserbased word association game that I built.


What Exactly is Next.js Middleware?

Next.js middleware is simply a piece of code that runs before your page or API handlers execute. It acts as a gateway or sits 'in the middle' between your user's request and your application. It is perfect for handling tasks like authentication checks, redirection, logging requests, or controlling access to certain pages. Think of it as being a little like a security guard at the door, checking credentials and deciding who gets in.

Unlike traditional middleware in frameworks like Express, Next.js middleware integrates directly into the application's structure, making it very straightforward to use.

Why Middleware Makes Route Protection Easier

The main benefit is simplicity. Instead of duplicating authentication logic in every API or page, middleware lets you write the logic once and apply it easily to multiple routes. It reduces duplication, keeps your code neat, and makes changes easy to manage.


Protecting Routes in Next.js with Middleware

In Next.js, adding middleware is as simple as creating a file called middleware.ts in your project's root or within specific route directories.

A Simple Middleware Example for Authentication

Here's a nice, simple example where we use middleware to check if a user is logged in by looking for a cookie:

// middleware.tsimport { NextRequest, NextResponse } from 'next/server';export function middleware(request: NextRequest) {  const token = request.cookies.get('authToken');  if (!token) {    return NextResponse.redirect(new URL('/login', request.url));  }  return NextResponse.next();}export const config = {  matcher: ['/dashboard/:path*', '/profile/:path*'],};

This middleware checks for an authentication cookie. If the cookie isn't there, the user is redirected to the login page. If the cookie is present, then the request proceeds as normal. In a production environment, we would flesh this out by storing authenticated cookies in a database and then checking the visitor's cookie against that (and other attributes), but for the sake of an example, this feels like it takes you about 90% of the way there and hopefully gives a little example of how middleware can be used.


Middleware for More Advanced Permissions

For a more advanced example, middleware obviously isn't limited to just checking login status; it can also handle authorisation, such as ensuring users have the correct roles or permissions before accessing a route.

For example:

// middleware.tsimport { NextRequest, NextResponse } from 'next/server';import { verifyUserToken } from './auth/utils';export async function middleware(request: NextRequest) {  const token = request.cookies.get('authToken');  const user = token ? await verifyUserToken(token) : null;  if (!user) {    return NextResponse.redirect(new URL('/login', request.url));  }  if (request.nextUrl.pathname.startsWith('/admin') && user.role !== 'admin') {    return NextResponse.redirect(new URL('/unauthorised', request.url));  }  return NextResponse.next();}export const config = {  matcher: ['/dashboard/:path*', '/admin/:path*'],};

I won't go into the nittygritty on how verifyUserToken might work for the sake of this illustration, but what I hope you can see is that this middleware clearly separates authentication (checking that the user is logged in) and authorisation (checking their role). In this way, we can ensure that users without admin rights cannot gain access to restricted admin areas.


Middleware in Action: Protecting Apis in Linkudo

Recently, I built Linkudo, a modern, browserbased reimagining of a famous word association game. The game relies heavily on multiple APIs for game configuration, gameplay, score submissions, and fetching game data. Because these APIs needed protection against misuse and unauthorised access, I used middleware in Next.js to ensure that every request was checked for validity before reaching the API handlers.

For example, in this project, middleware is used to:

  • Ratelimit access.
  • Handle CORS and method validation.
  • Verify player identity through authentication tokens.
  • Block suspicious or invalid requests early on.
  • Provide detailed logging and error tracking, making debugging simpler.

This approach meant that the code behind my API routes stayed clean and focused on handling game logic rather than repetitive security checks.


Common Middleware Pitfalls to Avoid

From my experience, there are two common mistakes that I've seen developers make with Next.js middleware:

Applying Middleware Too Widely

Middleware should be targeted carefully. Always use the matcher property to limit middleware to only the specific routes you need to apply middleware logic to. This prevents unexpected redirects or accidental blocking of public content.

Not Handling Errors or Edge Cases Clearly

Ensure your middleware gracefully manages cases like expired tokens or unexpected input. Middleware should redirect clearly or return meaningful error messages, avoiding confusion or infinite redirects.


Wrapping up

Middleware in Next.js is one of those tools that genuinely simplifies your code. It neatly handles authentication, authorisation, and other routelevel checks, helping you build secure applications without cluttering your logic. By carefully targeting middleware and clearly defining its scope, your Next.js apps become both secure and maintainable.

Key Takeaways

  • Middleware provides a clean, central place to manage authentication and authorisation.
  • It's ideal for protecting APIs and pages from unauthorised access or misuse.
  • Clearly define middleware scope using the matcher option.
  • Always consider edge cases and handle errors gracefully.

Middleware helps you keep your apps organised, secure, and simpler to manage, making route protection straightforward rather than cumbersome.


Categories:

  1. Development
  2. Front‑End Development
  3. JavaScript
  4. Next.js
  5. Node.js