Using Next.js Proxy for Route Protection

Hero image for Using Next.js Proxy for Route Protection. Image by Jamie Street.
Hero image for 'Using Next.js Proxy 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 — renamed Proxy in Next.js 16 — provides a clean, central place for optimistic checks and redirects. It does not replace authentication and authorisation beside protected server operations.

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 useful for optimistic authentication checks, redirection, logging requests, or coarse route filtering. Think of it as an early checkpoint rather than the final security guard: sensitive data access and mutations still need trusted checks in the server code that performs them.

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. Middleware lets you centralise preliminary routing checks and apply them to multiple routes. It reduces duplication and keeps redirect logic easy to manage, while each sensitive API, Server Action, or dataaccess function remains responsible for its own trusted authentication and authorisation checks.


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 simple example where middleware uses the presence of a cookie as an optimistic signal for redirecting a visitor:

// middleware.tsimport { NextRequest, NextResponse } from 'next/server';// This is an optimistic redirect, not a trusted authorisation check.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 whether an authentication cookie exists. If it is missing, the visitor is redirected to the login page; its presence alone does not prove identity or permission. Treat this as an early routing hint only. In production, use a signed or encrypted session and verify authentication and authorisation again beside every protected data access or mutation.


Middleware for More Advanced Permissions

For a more advanced example, middleware can inspect verified session claims and redirect users whose role does not match a route. This is still an optimistic route check: the protected server operation must make its own authorisation decision.

For example:

// middleware.tsimport { NextRequest, NextResponse } from 'next/server';import { verifyUserToken } from './auth/utils';// Repeat authentication and authorisation beside each protected operation.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 the example distinguishes authentication (checking who the user is) from authorisation (checking what they may do). The redirect improves the navigation experience, but it cannot by itself ensure that a user lacks access: the admin handler and its data layer must repeat the relevant trusted checks.


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 reject clearly invalid requests before they reached the API handlers. The handlers remained the trusted boundary for validating each sensitive operation.

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 kept shared preliminary checks out of the API routes, while the handlers still authenticated and authorised the sensitive operations they performed.


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 preliminary route checks. It can handle optimistic session checks, redirects, and other routelevel filtering without cluttering application logic. Security still depends on authenticating and authorising each sensitive operation close to the data it uses.

Key Takeaways

  • Middleware provides a clean, central place for optimistic route checks and redirects.
  • Protected APIs and mutations must still authenticate and authorise each sensitive operation on the server.
  • Clearly define middleware scope using the matcher option.
  • Always consider edge cases and handle errors gracefully.

Middleware helps you keep route filtering organised and simpler to manage, while trusted serverside checks protect the underlying data and operations.


Postscript

June 2026: Since this article was written, Next.js has renamed Middleware to Proxy for this use case. I'm keeping this article here because the routeprotection pattern is still useful, but for the current terminology and migration context see my article on Next.js Proxy replacing Middleware.


Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.