All The Ways to Handle Redirects in Next.js
A comprehensive guide to every method of redirecting users in Next.js, from basic server-side redirects to advanced client-side navigation patterns.


Redirects are a fundamental part of web development, and Next.js provides multiple ways to handle them. Whether you need to redirect users after a form submission, handle authentication flows, or implement SEO-friendly redirects, understanding all the available options is crucial for building robust applications.
In this comprehensive guide, we'll explore every method of redirecting users in Next.js, including their use cases, status codes, and best practices.
The Complete Redirect Methods Overview
Next.js offers five main approaches to handle redirects:
Method | Use Case | Context | Status Code | SEO Impact |
---|---|---|---|---|
redirect() | Conditional redirects, authentication checks, data validation | Server Components, Route Handlers, Server Actions | 307 (Temporary) or 303 (Server Actions) | Temporary redirect |
permanentRedirect() | Permanent URL changes | Server Components, Route Handlers, Server Actions | 308 (Permanent) | Permanent redirect |
NextResponse.redirect() | Conditional redirects with custom status | Route Handlers, Middleware | Custom (301, 302, 307, 308) | Flexible |
| Static URL redirects | Build time | 307/308 (configurable) | SEO-friendly |
useRouter().push() | Client-side navigation | Client Components | N/A | No SEO impact |
1. Server Component with redirect()
The redirect()
function is the most common way to redirect users in Next.js. It's designed for temporary redirects and always returns a 307 status code (or 303 in Server Actions).
Basic Usage
import { redirect } from "next/navigation";
export default async function ProfilePage({
params,
}: {
params: { id: string };
}) {
const user = await fetchUser(params.id);
// Redirect if user doesn't exist
if (!user) {
redirect("/login");
}
return <div>Welcome, {user.name}!</div>;
}
Key Characteristics
- Status Code: 307 (Temporary Redirect) or 303 (Server Actions)
- Method Preservation: Preserves the original HTTP method (POST remains POST)
- Use Cases: Authentication checks, data validation, conditional navigation
- Error Handling: Throws a
NEXT_REDIRECT
error that terminates rendering
Important Notes
// ❌ Don't use in try/catch blocks
try {
redirect("/dashboard");
} catch (error) {
// This won't work as expected
}
// ✅ Call outside try/catch
const result = await someOperation();
if (!result.success) {
redirect("/error");
}
2. Server Component with permanentRedirect()
Use permanentRedirect()
when you want to indicate that a URL has permanently moved. This returns a 308 status code.
When to Use
import { permanentRedirect } from "next/navigation";
export default async function OldProductPage({
params,
}: {
params: { oldId: string };
}) {
// Get the new product ID from database
const newProductId = await getNewProductId(params.oldId);
if (newProductId) {
// This product has permanently moved
permanentRedirect(`/products/${newProductId}`);
}
return <div>Product not found</div>;
}
SEO Benefits
- Search Engine Behavior: Search engines will update their indexes
- Link Equity: Passes SEO value to the new URL
- Permanent Change: Indicates the old URL should not be used anymore
3. Route Handler with Custom Status Codes
For maximum control over redirect behavior, use Route Handlers with NextResponse.redirect()
. This is the only method that allows you to specify custom status codes.
Basic Route Handler Redirect
// app/api/redirect/route.ts
import { NextRequest, NextResponse } from "next/server";
export function GET(request: NextRequest) {
// Always returns 308 (permanent redirect)
return NextResponse.redirect(new URL("/new-path", request.url));
}
Custom Status Code Redirects
// app/api/seo-redirect/route.ts
import { NextRequest, NextResponse } from "next/server";
export function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const destination = searchParams.get("to");
if (!destination) {
return NextResponse.json({ error: "Missing destination" }, { status: 400 });
}
// SEO-friendly 301 redirect for permanent moves
return NextResponse.redirect(
new URL(destination, request.url),
301, // Custom status code
);
}
Advanced Example with Conditional Logic
// app/api/smart-redirect/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const userAgent = request.headers.get("user-agent") || "";
const isMobile = /mobile/i.test(userAgent);
// Different redirects based on device
const destination = isMobile
? new URL("/mobile-app", request.url)
: new URL("/desktop-app", request.url);
// Use 302 for temporary device-based redirects
return NextResponse.redirect(destination, 302);
}
4. Next.js Config Redirects
Configure redirects at build time using next.config.js
. This is perfect for known URL changes and provides excellent performance.
Basic Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
// Basic redirect
{
source: "/old-about",
destination: "/about",
permanent: true, // 308 status code
},
// Wildcard matching
{
source: "/blog/:slug*",
destination: "/articles/:slug*",
permanent: true,
},
// Query string handling
{
source: "/search",
destination: "/find?q=:q",
permanent: false, // 307 status code
},
];
},
};
module.exports = nextConfig;
Advanced Patterns
// next.config.js
const nextConfig = {
async redirects() {
return [
// Conditional redirects based on headers
{
source: "/admin/:path*",
destination: "/login",
permanent: false,
has: [
{
type: "header",
key: "authorization",
value: "(?<token>.*)",
},
],
},
// Subdomain redirects
{
source: "/blog/:slug",
destination: "https://blog.example.com/:slug",
permanent: true,
has: [
{
type: "host",
value: "www.example.com",
},
],
},
];
},
};
5. Client-Side Navigation with useRouter()
For client-side redirects in response to user interactions, use the useRouter
hook.
Basic Usage
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function LoginForm() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (formData: FormData) => {
setIsLoading(true);
try {
const response = await fetch("/api/login", {
method: "POST",
body: formData,
});
if (response.ok) {
// Client-side redirect after successful login
router.push("/dashboard");
}
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsLoading(false);
}
};
return (
<form action={handleSubmit}>
{/* form fields */}
<button type="submit" disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign In"}
</button>
</form>
);
}
Navigation Options
The useRouter
hook provides several navigation methods, each serving different purposes in your application's user experience. Understanding when to use each method will help you create intuitive navigation flows that feel natural to your users.
The key difference between these methods is how they affect the browser's history stack. push()
adds a new entry to the history, allowing users to navigate back, while replace()
overwrites the current entry, preventing users from returning to the previous page. This distinction is crucial for authentication flows and multi-step processes.
"use client";
import { useRouter } from "next/navigation";
export default function NavigationComponent() {
const router = useRouter();
const handleNavigation = () => {
// Different navigation methods
router.push("/dashboard"); // Add to history
router.replace("/dashboard"); // Replace current entry
router.back(); // Go back
router.forward(); // Go forward
router.refresh(); // Refresh current page
};
return <button onClick={handleNavigation}>Navigate</button>;
}
Status Codes: Why They Matter
Understanding HTTP status codes is crucial for SEO and user experience:
301 vs 302 vs 307 vs 308
The choice of HTTP status code for redirects has significant implications for both SEO and application behavior. Understanding the differences between these codes is crucial for implementing redirects correctly.
The main distinction lies in two factors: permanence (temporary vs permanent) and method preservation (whether the HTTP method changes during redirect). Status codes 301 and 302 are legacy codes that will change POST requests to GET requests, while 307 and 308 are newer codes that preserve the original HTTP method.
// 301 - Permanent redirect (changes POST to GET)
NextResponse.redirect(url, 301);
// 302 - Temporary redirect (changes POST to GET)
NextResponse.redirect(url, 302);
// 307 - Temporary redirect (preserves method)
NextResponse.redirect(url, 307); // Default for redirect()
// 308 - Permanent redirect (preserves method)
NextResponse.redirect(url, 308); // Default for permanentRedirect()
SEO Implications
// ✅ Good for SEO - permanent change
export function GET() {
return NextResponse.redirect(
new URL("/new-location", request.url),
301, // Search engines update their indexes
);
}
// ⚠️ Neutral for SEO - temporary change
export function GET() {
return NextResponse.redirect(
new URL("/temporary-location", request.url),
307, // Search engines keep original URL
);
}
Middleware Redirects
Middleware redirects operate at the edge of your application, running before your pages and API routes are processed. This makes them incredibly powerful for implementing cross-cutting concerns like authentication, A/B testing, and geo-based redirects with minimal performance impact.
Unlike component-based redirects, middleware runs on every request matching your configuration, making it ideal for global redirect logic. The middleware function has access to the incoming request, allowing you to make decisions based on headers, cookies, URLs, and other request properties.
Handle redirects at the edge with middleware for better performance:
// middleware.ts
import { NextResponse, NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Authentication redirect
if (request.nextUrl.pathname.startsWith("/admin")) {
const token = request.cookies.get("auth-token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// A/B testing redirect
if (request.nextUrl.pathname === "/pricing") {
const variant = Math.random() > 0.5 ? "a" : "b";
return NextResponse.redirect(new URL(`/pricing-${variant}`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/admin/:path*", "/pricing"],
};
Anti-Patterns and Common Mistakes
❌ Using window.location
Instead of Next.js Methods
Using the browser's native window.location
API might seem like a quick solution, but it bypasses all of Next.js's optimizations and features. This approach forces a full page reload, losing the benefits of client-side navigation, prefetching, and the smooth user experience that Next.js provides.
Next.js navigation methods maintain the application state, preserve scroll position when appropriate, and work seamlessly with the framework's routing system. They also integrate properly with features like loading states and error boundaries.
// ❌ Don't do this - breaks Next.js optimizations
const handleRedirect = () => {
window.location.href = "/dashboard";
};
// ✅ Use Next.js navigation instead
const handleRedirect = () => {
router.push("/dashboard");
};
❌ Redirecting in Client Components During Render
One of the most common mistakes when working with redirects in Next.js is attempting to call server-side redirect functions from client components during the render phase. The redirect()
function is designed to work only in server-side contexts and will throw an error if called from a client component.
This pattern often occurs when developers try to implement authentication checks or conditional redirects in client components. The solution is to use React's useEffect
hook or event handlers, combined with the useRouter
hook for client-side navigation.
// ❌ Don't do this - causes issues
"use client";
export default function ClientComponent() {
// This will cause problems
if (someCondition) {
redirect("/somewhere"); // Error!
}
return <div>Content</div>;
}
// ✅ Use useEffect or event handlers instead
("use client");
export default function ClientComponent() {
const router = useRouter();
useEffect(() => {
if (someCondition) {
router.push("/somewhere");
}
}, [someCondition]);
return <div>Content</div>;
}
❌ Using Wrong Status Codes for SEO
Choosing the wrong HTTP status code for redirects can have lasting negative effects on your website's search engine rankings. Search engines interpret status codes as signals about the nature of the redirect, which directly impacts how they handle link equity, indexing, and ranking.
Using a permanent redirect (308 or 301) for temporary changes can confuse search engines and cause them to permanently replace the original URL in their index. Conversely, using temporary redirects for permanent changes means you won't transfer SEO value to the new URL.
// ❌ Wrong - using permanent redirect for temporary content
export function GET() {
return NextResponse.redirect(url, 308); // Wrong for temporary changes
}
// ✅ Right - using appropriate status code
export function GET() {
return NextResponse.redirect(url, 302); // Correct for temporary redirects
}
Performance Considerations
Server-Side vs Client-Side
The choice between server-side and client-side redirects significantly impacts your application's performance, user experience, and SEO. Server-side redirects happen before any HTML is sent to the browser, making them faster and more SEO-friendly, while client-side redirects require JavaScript execution and a full page render.
Server-side redirects are ideal for authentication checks, data validation, and any logic that can be determined before rendering. They prevent unnecessary loading states and provide immediate feedback to users. Client-side redirects are better suited for interactive scenarios where you need to show loading states, handle user feedback, or perform client-only operations.
// ✅ Fast - handled at server level
export default async function Page() {
const user = await getUser();
if (!user) {
redirect("/login"); // Server-side redirect
}
return <Dashboard user={user} />;
}
// ⚠️ Slower - requires client-side JavaScript
("use client");
export default function Page() {
const { user, isLoading } = useUser();
const router = useRouter();
if (isLoading) return <Loading />;
if (!user) {
router.push("/login"); // Client-side redirect
return null;
}
return <Dashboard user={user} />;
}
Real-World Examples
Authentication Flow
Authentication redirects are one of the most common use cases for server-side redirects. By checking authentication status on the server, you can immediately redirect unauthenticated users without exposing protected content or requiring a client-side redirect after the page loads.
This pattern ensures that protected routes are secure by default and provides a better user experience by eliminating loading flashes and unnecessary API calls for unauthenticated users.
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
export default async function DashboardPage() {
const session = await getServerSession();
if (!session) {
redirect("/api/auth/signin");
}
return <Dashboard user={session.user} />;
}
URL Migration
When restructuring your website or changing URL patterns, configuration-based redirects in next.config.js
provide the most efficient solution. These redirects are handled at the build level and don't require any runtime processing, making them extremely fast and SEO-friendly.
The key to successful URL migration is using permanent redirects (status 308) to ensure search engines transfer all ranking signals from old URLs to new ones. This preserves your SEO investment and prevents broken links.
// next.config.js
const nextConfig = {
async redirects() {
return [
{
source: "/old-blog/:slug",
destination: "/articles/:slug",
permanent: true, // 308 - tells search engines it's permanent
},
];
},
};
Form Success Redirect
Server Actions provide an elegant way to handle form submissions and subsequent redirects in a single server-side operation. This pattern is particularly powerful because it combines data mutation, cache revalidation, and user redirection in one seamless flow.
The redirect after a successful form submission prevents the common issue of duplicate submissions when users refresh the page, following the Post-Redirect-Get pattern that's considered a web development best practice.
// app/actions.ts
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
// Create post in database
const post = await createPostInDB({ title });
// Revalidate the posts page
revalidatePath("/posts");
// Redirect to the new post (307 status code)
redirect(`/posts/${post.id}`);
}
Best Practices Summary
- Use
redirect()
for temporary redirects after user actions - Use
permanentRedirect()
for permanent URL changes - Use Route Handlers with custom status codes for SEO-critical redirects
- Use
next.config.js
redirects for known static redirects - Use
useRouter()
for client-side navigation in response to user events - Choose the right status code based on whether the redirect is temporary or permanent
- Avoid
window.location
in favor of Next.js methods - Handle redirects at the server level when possible for better performance
Conclusion
Next.js provides powerful and flexible redirect capabilities that cover every use case from simple page redirects to complex conditional logic. Understanding when to use each method will help you build better user experiences while maintaining good SEO practices.
Remember: the key is choosing the right tool for the job. Server-side redirects are faster and better for SEO, while client-side redirects offer more interactivity. Status codes matter for search engines, and using the appropriate method for your use case will ensure your application performs optimally.