Introduction
Every production app has routes that only certain users should access. Whether it's a dashboard, admin panel, or user settings, route protection is your first defense against unauthorized access. Proxy makes this elegant and performant.
Key Concepts
Route protection is the practice of restricting access to certain URL paths based on authentication or authorization status. In Next.js, this happens at the proxy layer—before the page even starts rendering.
The protection flow:
- User requests a protected route
- Proxy checks for valid authentication (usually a cookie)
- If valid → allow access
- If invalid → redirect to login
Real World Context
Without route protection:
- Users could access
/dashboardby typing the URL directly - Sensitive data could be exposed during the initial page load
- Server resources would be wasted rendering pages for unauthorized users
Proxy-level protection means unauthenticated users never even see a flash of protected content.
Deep Dive
Basic Protection Pattern
typescriptimport { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function proxy(request: NextRequest) { const token = request.cookies.get('session')?.value; const { pathname } = request.nextUrl; // Define protected routes const protectedPaths = ['/dashboard', '/settings', '/account']; const isProtected = protectedPaths.some(path => pathname.startsWith(path)); // If no token and trying to access protected route if (!token && isProtected) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('from', pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*', '/settings/:path*', '/account/:path*'], };
Preserving the Return URL
The from query parameter remembers where the user wanted to go:
typescript// In proxy - save original destination loginUrl.searchParams.set('from', pathname); // In login page - redirect back after success const from = searchParams.get('from') || '/dashboard'; router.push(from);
Token Validation
Simply checking for a cookie's existence isn't enough for production:
typescriptimport { jwtVerify } from 'jose'; const secret = new TextEncoder().encode(process.env.JWT_SECRET); export async function proxy(request: NextRequest) { const token = request.cookies.get('session')?.value; if (!token) { return NextResponse.redirect(new URL('/login', request.url)); } try { // Actually verify the token is valid await jwtVerify(token, secret); return NextResponse.next(); } catch { // Token is invalid or expired const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('session'); // Clear invalid cookie return response; } }
Common Pitfalls
-
Only checking cookie existence: A malicious user could set any cookie value. Always validate tokens server-side.
-
Redirect loops: If
/loginis accidentally included in protected routes, users get stuck in an infinite redirect loop. -
Forgetting the matcher: Without a proper matcher, proxy runs on every request, including the login page itself.
Best Practices
- Always validate tokens: Don't just check if a cookie exists—verify its signature and expiration
- Use HTTPS in production: Cookies should have the
secureflag set - Clear invalid cookies: If a token fails validation, delete it to prevent repeated failures
- Preserve query parameters: Include the full URL (with query params) in the
fromparameter
Summary
Route protection with proxy ensures unauthenticated users are redirected before any protected content loads. Check for session tokens, validate them properly, and always preserve the original URL for post-login redirects. Use matchers to limit proxy execution to protected routes only.