Not all pages should be accessible to everyone. Some routes require authentication, others need specific roles or permissions. Redirects send users to different pages based on conditions, while navigation guards protect routes from unauthorized access. Whether you're building login flows, protecting admin panels, or handling permissions, Next.js provides powerful tools for secure navigation. Let's master redirects and route protection!
Redirect Basics
Server Component Redirects
Use redirect() in Server Components and Server Actions:
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
async function getUser() {
const sessionCookie = cookies().get('session');
if (!sessionCookie) return null;
// Verify session and get user
const user = await verifySession(sessionCookie.value);
return user;
}
export default async function DashboardPage() {
const user = await getUser();
// Redirect if not authenticated
if (!user) {
redirect('/login');
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>This is your dashboard.</p>
</div>
);
}
// ✅ Server-side redirect
// ✅ Runs before page renders
// ✅ SEO-friendly
// ✅ No flash of protected contentClient Component Redirects
Use useRouter in Client Components:
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function LoginForm() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
// Redirect to dashboard after successful login
router.replace('/dashboard');
} else {
alert('Login failed');
}
} catch (error) {
alert('An error occurred');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="email"
name="email"
placeholder="Email"
required
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="password"
name="password"
placeholder="Password"
required
className="w-full px-4 py-2 border rounded-lg"
/>
<button
type="submit"
disabled={loading}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
// ✅ Client-side redirect after login
// ✅ Use router.replace() so user can't go back to login
// ✅ Handle loading statePermanent Redirects
import { permanentRedirect } from 'next/navigation';
export default function OldBlogPage() {
// Content moved permanently to new location
permanentRedirect('/blog');
}
// Use permanentRedirect() for:
// ✅ Moved content that won't change back
// ✅ Old URLs that should be updated in search engines
// ✅ Deprecated routes
// Returns 308 status code (permanent redirect)
// Search engines update their indexes
// Browsers cache the redirect
// Use redirect() for:
// ✅ Temporary redirects
// ✅ Authentication redirects
// ✅ Conditional redirects
// Returns 307 status code (temporary redirect)
// Search engines don't update indexesMiddleware for Route Protection
Middleware runs before requests reach your pages, making it ideal for authentication:
Basic Authentication Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Get session token from cookie
const token = request.cookies.get('session')?.value;
// Check if accessing protected route
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtectedRoute && !token) {
// Redirect to login if not authenticated
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
// Specify which routes to run middleware on
export const config = {
matcher: [
'/dashboard/:path*',
'/admin/:path*',
'/profile/:path*',
],
};
// ✅ Runs before page loads
// ✅ Protects multiple routes at once
// ✅ Adds redirect parameter for return after login
// ✅ Efficient - runs on Edge RuntimeAdvanced Middleware with Role Checks
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
const { pathname } = request.nextUrl;
// Public routes - allow without authentication
const publicRoutes = ['/', '/login', '/signup', '/about'];
if (publicRoutes.includes(pathname)) {
return NextResponse.next();
}
// Check authentication
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Verify token and get user info
const user = await verifyToken(token);
if (!user) {
// Invalid token - clear cookie and redirect
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('session');
return response;
}
// Admin-only routes
if (pathname.startsWith('/admin')) {
if (user.role !== 'admin') {
// Not authorized - redirect to dashboard
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
// Add user info to request headers (accessible in Server Components)
const response = NextResponse.next();
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);
return response;
}
export const config = {
matcher: [
// Protected routes
'/dashboard/:path*',
'/admin/:path*',
'/profile/:path*',
'/settings/:path*',
],
};
// ✅ Authentication check
// ✅ Role-based access control
// ✅ Invalid token handling
// ✅ User info in headers
// ✅ Granular route protectionAuthentication Patterns
Pattern 1: Protected Page with Auth Check
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
async function getSession() {
const sessionCookie = cookies().get('session');
if (!sessionCookie) return null;
try {
const session = await verifySession(sessionCookie.value);
return session;
} catch {
return null;
}
}
export default async function DashboardPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
// Fetch user-specific data
const userData = await fetch(
`https://api.example.com/users/${session.userId}`,
{ cache: 'no-store' }
).then(r => r.json());
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">
Welcome, {userData.name}!
</h1>
<div className="grid grid-cols-3 gap-6">
<StatCard title="Orders" value={userData.orderCount} />
<StatCard title="Points" value={userData.points} />
<StatCard title="Level" value={userData.level} />
</div>
</div>
);
}
// ✅ Server-side authentication check
// ✅ Redirect before rendering
// ✅ Fetch user-specific data after auth
// ✅ No protected content leaksPattern 2: Login with Redirect Back
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
import LoginForm from './LoginForm';
async function getSession() {
const sessionCookie = cookies().get('session');
if (!sessionCookie) return null;
return await verifySession(sessionCookie.value);
}
export default async function LoginPage({
searchParams,
}: {
searchParams: { redirect?: string };
}) {
// If already logged in, redirect
const session = await getSession();
if (session) {
redirect(searchParams.redirect || '/dashboard');
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md w-full">
<h1 className="text-3xl font-bold mb-8 text-center">
Login
</h1>
<LoginForm redirectTo={searchParams.redirect} />
</div>
</div>
);
}
// ✅ Redirect if already authenticated
// ✅ Preserve redirect parameter
// ✅ Return to intended page after login'use client';
import { useRouter } from 'next/navigation';
export default function LoginForm({
redirectTo
}: {
redirectTo?: string
}) {
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Login logic...
const success = await login(email, password);
if (success) {
// Redirect to intended page or dashboard
router.replace(redirectTo || '/dashboard');
}
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
// Flow:
// 1. User tries to access /dashboard
// 2. Middleware redirects to /login?redirect=/dashboard
// 3. User logs in
// 4. Redirects back to /dashboard
// ✅ Preserves user's intended destination
// ✅ Better user experience
// ✅ Works with bookmarked protected URLsPattern 3: Role-Based Access
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
async function getUser() {
const sessionCookie = cookies().get('session');
if (!sessionCookie) return null;
const session = await verifySession(sessionCookie.value);
if (!session) return null;
const user = await getUserById(session.userId);
return user;
}
export default async function AdminPage() {
const user = await getUser();
// Check authentication
if (!user) {
redirect('/login');
}
// Check authorization
if (user.role !== 'admin') {
redirect('/dashboard'); // Or show 403 error
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-lg mb-6">
<p className="text-sm text-yellow-800">
⚠️ You're in the admin panel. Be careful!
</p>
</div>
<AdminDashboard />
</div>
);
}
// ✅ Two-step check: authentication + authorization
// ✅ Clear separation of concerns
// ✅ Redirects non-admins to safe page
// ✅ Visual warning for adminsRedirects in Server Actions
Form Submission with Redirect
'use server';
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
// Check authentication
const session = cookies().get('session');
if (!session) {
redirect('/login');
}
// Extract form data
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validate
if (!title || !content) {
throw new Error('Title and content are required');
}
// Create post
const post = await db.posts.create({
data: {
title,
content,
authorId: session.value,
},
});
// Revalidate blog page
revalidatePath('/blog');
// Redirect to the new post
redirect(`/blog/${post.slug}`);
}
// ✅ Server Action with redirect
// ✅ Authentication check
// ✅ Revalidate cache
// ✅ Redirect to created resourceMulti-Step Form with Conditional Redirects
'use server';
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
export async function saveOnboardingStep(
step: number,
formData: FormData
) {
const session = cookies().get('session');
if (!session) {
redirect('/login');
}
// Save step data
await saveStepData(session.value, step, formData);
// Determine next step
if (step === 1) {
redirect('/onboarding/step-2');
} else if (step === 2) {
redirect('/onboarding/step-3');
} else if (step === 3) {
// Onboarding complete
await markOnboardingComplete(session.value);
redirect('/dashboard');
}
}
// ✅ Handles multi-step flow
// ✅ Conditional redirects based on step
// ✅ Completes onboarding on final stepComplete Authentication Examples
Example 1: Complete Auth Guard Utility
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
interface User {
id: string;
email: string;
name: string;
role: 'user' | 'admin';
}
export async function requireAuth(): Promise<User> {
const sessionCookie = cookies().get('session');
if (!sessionCookie) {
redirect('/login');
}
try {
const user = await verifySession(sessionCookie.value);
if (!user) {
redirect('/login');
}
return user;
} catch {
redirect('/login');
}
}
export async function requireAdmin(): Promise<User> {
const user = await requireAuth();
if (user.role !== 'admin') {
redirect('/dashboard');
}
return user;
}
export async function requireRole(
allowedRoles: string[]
): Promise<User> {
const user = await requireAuth();
if (!allowedRoles.includes(user.role)) {
redirect('/dashboard');
}
return user;
}
// Usage in pages:
export default async function DashboardPage() {
const user = await requireAuth();
return <div>Welcome {user.name}</div>;
}
export default async function AdminPage() {
const admin = await requireAdmin();
return <div>Admin Panel</div>;
}
export default async function ModeratorPage() {
const user = await requireRole(['admin', 'moderator']);
return <div>Moderator Tools</div>;
}
// ✅ Reusable auth guards
// ✅ Type-safe user object
// ✅ Flexible role checking
// ✅ Clean page codeExample 2: Guest-Only Routes (Redirect if Authenticated)
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
async function getSession() {
const sessionCookie = cookies().get('session');
if (!sessionCookie) return null;
try {
return await verifySession(sessionCookie.value);
} catch {
return null;
}
}
export default async function LoginPage() {
const session = await getSession();
// Redirect to dashboard if already authenticated
if (session) {
redirect('/dashboard');
}
return (
<div className="min-h-screen flex items-center justify-center">
<LoginForm />
</div>
);
}
// Same pattern for:
// - /signup
// - /forgot-password
// - /reset-password
// ✅ Prevents authenticated users from seeing login
// ✅ Automatic redirect to dashboard
// ✅ Better UX - no unnecessary formsExample 3: Subscription/Payment Guard
import { redirect } from 'next/navigation';
import { requireAuth } from '@/lib/auth-guards';
async function getSubscription(userId: string) {
const subscription = await db.subscriptions.findFirst({
where: {
userId,
status: 'active',
},
});
return subscription;
}
export default async function PremiumPage() {
const user = await requireAuth();
// Check subscription
const subscription = await getSubscription(user.id);
if (!subscription) {
redirect('/pricing?upgrade=true');
}
return (
<div className="container mx-auto px-4 py-8">
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-lg mb-6">
<p className="text-sm text-yellow-800">
✨ Premium Feature - Thanks for subscribing!
</p>
</div>
<PremiumFeatures />
</div>
);
}
// ✅ Two-level protection: auth + subscription
// ✅ Redirect to pricing with upgrade parameter
// ✅ Clear premium indicatorAuthentication & Guards Structure
Project organization with middleware and auth guards
Select a file or folder to see details
Best Practices
1. Use Middleware for App-Wide Protection
// ✅ GOOD: Middleware protects multiple routes
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
// Efficient - runs once on Edge
// Protects entire sections
// ❌ BAD: Auth check in every page
// Repetitive, error-prone, harder to maintain2. Always Use router.replace() After Login
// ✅ GOOD: Replace so user can't go back to login
router.replace('/dashboard');
// ❌ BAD: Push allows back button to login
router.push('/dashboard');
// User can press back and see login form again!3. Preserve Intended Destination
// ✅ GOOD: Save where user was trying to go
redirect(`/login?redirect=${pathname}`);
// After login, redirect back:
router.replace(searchParams.get('redirect') || '/dashboard');
// ✅ Better UX - user returns to intended page4. Clear Cookies on Invalid Session
// ✅ GOOD: Clear invalid cookies
const user = await verifyToken(token);
if (!user) {
const response = NextResponse.redirect('/login');
response.cookies.delete('session'); // Clear invalid cookie
return response;
}
// Prevents repeated verification attempts5. Use Appropriate Redirect Type
// ✅ Temporary redirects (307)
redirect('/login'); // Auth checks
redirect('/dashboard'); // Post-login
// ✅ Permanent redirects (308)
permanentRedirect('/blog'); // Moved content
permanentRedirect('/new-url'); // URL structure change
// Use permanent only when content truly moved permanentlyKey Takeaways
- redirect() - Server Components & Server Actions
- router.replace() - Client Components (use after login)
- Middleware - app-wide route protection
- permanentRedirect() - for moved content (308 status)
- Auth guards - reusable requireAuth() utilities
- Role checks - verify user permissions
- Preserve redirect - save intended destination
- Two-step protection - authentication + authorization
🎉 Navigation Section Complete!
You've completed the Navigation and Links section! You've mastered:
- ✅ Link component for client-side navigation
- ✅ useRouter hook for programmatic navigation
- ✅ usePathname and useSearchParams for URL state
- ✅ Active links and navigation states
- ✅ Redirects and navigation guards
You now have complete mastery of navigation in Next.js! You can build applications with smooth navigation, active link highlighting, loading states, and secure route protection. These skills are essential for creating professional, user-friendly Next.js applications with proper authentication flows.
🔒 Security First
Always implement authentication checks on the server (middleware or Server Components), not just the client. Client-side checks can be bypassed, but server-side checks ensure true security. Use middleware for efficiency and comprehensive protection!