If you're building a modern web app with Next.js App Router, you'll likely need to protect certain pages—like /dashboard or /account—from unauthenticated access.

In this post, you'll learn how to protect private routes using Middleware in Next.js 13+ (with the new app/ directory).


🧭 What do we mean by private routes?

Private routes are pages that should only be accessible if the user is authenticated.

Example:

/login → anyone can see it
/register → anyone can register
🔒 /dashboard → only logged-in users
🔒 /settings → must be authenticated

If a user isn't logged in, they should be redirected—usually to /login.


⚙️ App router vs pages router

In Pages Router (pages/ folder), route protection was usually done via getServerSideProps or HOCs.

With the App Router (app/ folder), the new recommendation is to use Middleware for global auth checks.


🧪 Step-by-step: protecting routes with middleware

1. Create your middleware

In the root of your project, create a file:

/middleware.ts

Basic structure:

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

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth_token');

  const isAuthenticated = !!token?.value;

  const protectedPaths = ['/dashboard', '/settings'];

  const pathname = request.nextUrl.pathname;

  const isProtected = protectedPaths.some((path) =>
    pathname.startsWith(path)
  );

  if (isProtected && !isAuthenticated) {
    const loginUrl = new URL('/login', request.url);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

This checks for a cookie called auth_token and redirects to /login if it's missing.


2. Configure your middleware.ts matcher

At the bottom of your file (or in next.config.js), you can limit where middleware applies:

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

This means the middleware only runs on the routes you specify.


3. Set the cookie on login

In your login logic (e.g. inside an API route or app/login/actions.ts), set the auth token in a cookie:

import { cookies } from 'next/headers';

export async function login(email: string, password: string) {
  // Your auth logic here
  const token = await getTokenFromServer(email, password);

  cookies().set('auth_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
  });
}
> You can also use JWTs or session tokens stored in secure HTTP-only cookies.

🛑 What middleware can _not_ do

  • ❌ You can’t access localStorage or window (it runs on the Edge)
  • ❌ You can’t fetch server-side session data unless you pass it via cookies or headers
  • ✅ You can read cookies and request URLs

🧠 Pro tips

  • Use matcher to limit the scope of your middleware
  • Protect both frontend and API routes if needed
  • Prefer secure, HTTP-only cookies for auth tokens
  • For more complex logic, integrate NextAuth.js or your own session system

✅ Summary checklist

  • ✅ Add a middleware.ts at the root
  • ✅ Read the auth_token from cookies
  • ✅ Redirect unauthenticated users to /login
  • ✅ Use matcher to target private routes
  • ✅ Set auth cookie on login, and clear it on logout

🔚 Conclusion

With Next.js App Router and Middleware, protecting private routes is lightweight and powerful. It’s one of the best ways to guard your pages on the edge—before they even render.

Using this pattern, you can create fast, secure apps that feel seamless for users while keeping sensitive content safe.