Real World Example

Protecting an Admin Area

SaaS dashboards (Razorpay Dashboard, Zoho, Freshworks) redirect logged-out users to login, keep the target URL, and block non-admins from admin pages early. ShopEase middleware decides the outcome from the cookie and a role claim:

Example

The decision logic of the middleware as a pure function (easy to unit test):

type Session = { userId: string; role: 'customer' | 'admin' } | null;
type Decision = { action: 'next' } | { action: 'redirect'; to: string };

function decide(pathname: string, session: Session): Decision {
  const needsLogin = ['/account', '/checkout', '/admin'].some((p) => pathname === p || pathname.startsWith(p + '/'));
  if (needsLogin && !session) {
    return { action: 'redirect', to: '/login?next=' + encodeURIComponent(pathname) };
  }
  if (pathname.startsWith('/admin') && session?.role !== 'admin') {
    return { action: 'redirect', to: '/403' };
  }
  if (pathname === '/login' && session) return { action: 'redirect', to: '/account' };
  return { action: 'next' };
}

const customer: Session = { userId: 'u1', role: 'customer' };
const admin: Session = { userId: 'u2', role: 'admin' };
const cases: [string, Session][] = [
  ['/products/42', null], ['/checkout', null], ['/admin/orders', customer],
  ['/admin/orders', admin], ['/login', customer], ['/account/orders', customer],
];
for (const [path, s] of cases) {
  console.log(path.padEnd(16), (s?.role ?? 'guest').padEnd(9), JSON.stringify(decide(path, s)));
}

Output

/products/42     guest     {"action":"next"}
/checkout        guest     {"action":"redirect","to":"/login?next=%2Fcheckout"}
/admin/orders    customer  {"action":"redirect","to":"/403"}
/admin/orders    admin     {"action":"next"}
/login           customer  {"action":"redirect","to":"/account"}
/account/orders  customer  {"action":"next"}

Wiring It Up

// middleware.ts  (proxy.ts in Next.js 16)
import { NextRequest, NextResponse } from 'next/server';
import { decide } from '@/lib/access';
import { readSession } from '@/lib/session'; // verifies a signed cookie, no DB call

export async function middleware(req: NextRequest) {
  const session = await readSession(req.cookies.get('session')?.value);
  const d = decide(req.nextUrl.pathname, session);
  return d.action === 'redirect' ? NextResponse.redirect(new URL(d.to, req.url)) : NextResponse.next();
}

export const config = { matcher: ['/account/:path*', '/checkout/:path*', '/admin/:path*', '/login'] };