AiTechWorlds
AiTechWorlds
Protecting routes in Next.js has two layers: middleware for fast edge-level checks, and server-side auth verification in layouts and pages. Understanding both ensures your app is secure without creating a confusing user experience.
Middleware runs before any page is rendered. It's the ideal place to check authentication for every request to protected routes, before any database query or server-side rendering happens.
// src/middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export default auth(function middleware(req) {
const { nextUrl, auth: session } = req;
const isLoggedIn = !!session;
// Define protected route patterns
const isProtected = nextUrl.pathname.startsWith("/dashboard") ||
nextUrl.pathname.startsWith("/settings") ||
nextUrl.pathname.startsWith("/api/protected");
const isAuthPage = nextUrl.pathname.startsWith("/login") ||
nextUrl.pathname.startsWith("/register");
// Redirect unauthenticated users from protected routes
if (isProtected && !isLoggedIn) {
const redirectUrl = new URL("/login", nextUrl.origin);
redirectUrl.searchParams.set("callbackUrl", nextUrl.pathname);
return NextResponse.redirect(redirectUrl);
}
// Redirect authenticated users away from auth pages
if (isAuthPage && isLoggedIn) {
return NextResponse.redirect(new URL("/dashboard", nextUrl.origin));
}
return NextResponse.next();
});
// Tell Next.js which routes to run middleware on
export const config = {
matcher: [
// Match all routes except static files and Next.js internals
"/((?!_next/static|_next/image|favicon.ico|public/).*)",
],
};
// src/middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth(function middleware(req) {
const session = req.auth;
const { pathname } = req.nextUrl;
// Admin-only routes
if (pathname.startsWith("/admin")) {
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
if (session.user.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
}
// Instructor-only routes
if (pathname.startsWith("/studio")) {
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
if (!["admin", "instructor"].includes(session.user.role)) {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
}
// Regular protected routes
if (pathname.startsWith("/dashboard")) {
if (!session) {
const url = new URL("/login", req.url);
url.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(url);
}
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!_next|api/auth|_vercel|.*\\..*).*)", "/api/:path*"],
};
Middleware is the first check. Layouts provide a second layer — useful when the layout itself needs auth data to render correctly:
// src/app/dashboard/layout.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
// Double-check auth in the layout (middleware might have race conditions during deploy)
if (!session) {
redirect("/login");
}
return (
<div className="flex min-h-screen">
<DashboardSidebar user={session.user} />
<main className="flex-1 bg-gray-50">{children}</main>
</div>
);
}
This is important: middleware runs at the edge and might have stale session data during Vercel deployment rollovers. The layout check ensures correctness.
For pages that need fine-grained checks beyond "is logged in":
// src/app/courses/[slug]/edit/page.tsx
import { auth } from "@/auth";
import { redirect, notFound } from "next/navigation";
import { db } from "@/lib/db";
export default async function EditCoursePage({ params }: { params: { slug: string } }) {
const session = await auth();
if (!session) redirect("/login");
const course = await db.course.findUnique({
where: { slug: params.slug },
select: { id: true, title: true, authorId: true },
});
if (!course) notFound();
// Only the author or an admin can edit
if (course.authorId !== session.user.id && session.user.role !== "admin") {
redirect("/unauthorized");
}
return <CourseEditor courseId={course.id} />;
}
For API routes in the App Router:
// src/app/api/courses/[id]/route.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const course = await db.course.findUnique({ where: { id: params.id } });
if (!course) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
if (course.authorId !== session.user.id && session.user.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
await db.course.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
}
After login, redirect users back to what they were trying to access:
// src/app/login/page.tsx
"use client";
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
export default function LoginPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
callbackUrl, // redirect to original destination after login
});
}
return (
<form onSubmit={handleLogin}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
</form>
);
}
User tries to visit /dashboard/settings → Middleware redirects to /login?callbackUrl=/dashboard/settings → User logs in → Redirects to /dashboard/settings.
// src/app/unauthorized/page.tsx
import Link from "next/link";
export default function UnauthorizedPage() {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Access Denied</h1>
<p className="text-gray-500 mt-3">
You don't have permission to access this page.
</p>
<div className="flex gap-3 justify-center mt-6">
<Link href="/dashboard" className="text-blue-600 hover:underline">
Go to Dashboard
</Link>
<span className="text-gray-300">|</span>
<Link href="/" className="text-blue-600 hover:underline">
Go Home
</Link>
</div>
</div>
</main>
);
}
Next lesson: Tailwind CSS in Next.js — typography plugin, advanced theming, and component patterns.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises