ProgrUmar Logo
Module 5: Authentication & Authorization

Protecting Routes with Middleware

Duration: 14 mins

Middleware-based Route Protection

The middleware.ts file runs on the Edge before every matched request. Check the session token and redirect to /login if it's missing — no protected page HTML ever reaches an unauthenticated user.

Protecting Routes with Middleware

Validating session access inside every individual page or API layout leads to duplicate verification scripts and high maintenance overhead. Next.js solves this by providing a centralized middleware.ts file. Middleware executes on the Edge before any incoming HTTP request resolves to a page layout. In this lesson, we will explore how to write robust route filters, manage path redirection workflows without entering infinite loop cycles, and secure private dashboard networks globally.


1. The Role of Middleware in Next.js Routing

Next.js Middleware intercepts every incoming request. It evaluates cookies, headers, and request URLs, deciding whether to rewrite the path, attach custom headers, or trigger an immediate HTTP redirect.

Key Architectural Advantages:

  • Edge-level Performance: Redirects happen before Next.js compiles page layouts or runs backend data queries, reducing server workload.
  • Zero Layout Flashing: Redirections occur at the network layer, preventing the client's browser from rendering half-loaded screens.
  • Centralized Access Control: Modify a single file to update permission models across thousands of routes.

2. The Global middleware.ts Location

To define middleware, create a file named middleware.ts (or .js) directly in the root of your project directory (or inside src/ if you use a src folder layout).

my-app/
├── app/
├── src/
│   ├── auth.ts
│   └── middleware.ts   ← Place it here alongside app folder
├── package.json
└── tsconfig.json

3. Defining the Route Matcher Configuration

By default, middleware intercepts every single route—including static images (/logo.png) and styling bundles (/_next/static/). This degrades performance. We use a config object with a matcher array to scope middleware execution:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Logic here only runs on paths matching the matcher configuration below
  return NextResponse.next();
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

4. Global Integration with Auth.js v5 Middleware

If you use Auth.js v5, securing your routes requires very little boilerplate. You simply export the auth handler generated in your config. Auth.js wraps your middleware logic automatically:

// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

// NextAuth will automatically run callbacks.authorized checks defined in auth.config
export default NextAuth(authConfig).auth;

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/settings/:path*'],
};

5. Writing Custom Route Redirection Rules

If your application manages a custom authentication cookie, you can write conditional redirects using the native NextResponse APIs:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionToken = request.cookies.get('session_token')?.value;
  const { pathname } = request.nextUrl;

  // Protect dashboard routes
  if (pathname.startsWith('/dashboard')) {
    if (!sessionToken) {
      // Redirect to login page and store original target in query params
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('callbackUrl', pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  // Redirect logged-in users away from auth pages (login/signup)
  if (pathname.startsWith('/login') || pathname.startsWith('/register')) {
    if (sessionToken) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.next(); // Continue request flow normally
}

export const config = {
  matcher: ['/dashboard/:path*', '/login', '/register'],
};

6. Attaching Request Headers in Middleware

You can use middleware to inspect request headers and attach new custom values (for instance, mapping geolocation features or user IDs) before passing the request to child Server Components:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers);
  
  // Attach user location inferred from hosting CDN geolocation headers
  const country = request.cookies.get('user_country')?.value || 'US';
  requestHeaders.set('x-user-country', country);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

You can now retrieve this header value inside any downstream async Server Component:

// app/dashboard/page.tsx
import { headers } from 'next/headers';

export default async function DashboardPage() {
  const headersList = await headers();
  const country = headersList.get('x-user-country');

  return <div>Targeting catalog: {country}</div>;
}

7. The Danger of Infinite Redirect Loops

An infinite redirect loop happens when your middleware redirects a user to a page, and that redirect page itself triggers the middleware, causing another redirect.

Example Loop: A user requests /dashboard. The middleware detects they aren't logged in and redirects them to /login. However, the middleware also checks /login, sees the user is still not logged in, and redirects to /login again.

Prevention: Ensure your matching statements or config.matcher patterns explicitly exclude authentication routes (like /login, /signup) from receiving redirect logic:

// Explicit check inside middleware to prevent loop
if (pathname === '/login') {
  return NextResponse.next();
}

8. Debugging Middleware Executions

Because middleware executes on edge nodes, standard Node.js server console hooks might run inside different deployment logs depending on your hosting provider. During local development, console outputs appear directly inside your terminal window running npm run dev.

Ensure you keep execution logic minimal. Running large queries or invoking heavy packages inside middleware will increase the time-to-first-byte (TTFB) score of every page request.


9. Common Gotchas

  • Matching Static Files: Forgetting to exclude assets (images, static bundles) inside the matcher configuration will trigger cookie checks on every request, degrading page performance and throwing authentication redirect errors on icon assets.
  • Attempting Database Queries inside Middleware: Traditional databases (like Postgres clients) require Node.js sockets that cannot resolve inside the Edge Runtime. If your middleware needs session data, retrieve it using standard JWT verification or lightweight HTTP fetch requests instead.
  • Header Modification Limits: You cannot modify response bodies inside middleware. You can only set request/response headers or cookies.

Key Takeaways

  • Mount the middleware.ts file at the root of your project alongside the app/ directory.
  • Leverage config.matcher to restrict middleware execution only to private paths.
  • Auth.js v5 provides native integration by exporting its auth handler directly into middleware configurations.
  • Always sanitize redirect logic to prevent recursive loops on login or registration pages.
  • Avoid heavy database operations or large imports inside middleware to keep request times low.

Restricting general page access protects your platform from outside traffic. But a production SaaS needs granular access control—differentiating customers from editors and admins. In the next lesson, we will cover Role-based Access Control (RBAC), extending session profiles to gate layout elements and secure Server Actions dynamically.

Chat with us