Next.js Route Groups: Public & Protected Routes Architecture
1. What Are Route Groups?
Route groups let you organize routes into logical folders without affecting the URL. A folder wrapped in parentheses (groupName) is invisible in the browser address bar.
File Structure: URL Result:
app/
├── (public)/
│ ├── page.tsx → /
│ ├── about/
│ │ └── page.tsx → /about
│ └── courses/
│ └── page.tsx → /courses
│
└── (protected)/
├── dashboard/
│ └── page.tsx → /dashboard
├── profile/
│ └── page.tsx → /profile
└── settings/
└── page.tsx → /settings
Notice that (public) and (protected) do not appear in any URL. They exist purely for code organization and layout separation.
2. Why Are Route Groups Needed?
Without route groups, you'd face these problems:
❌ Problem 1: Layout Pollution
Every page under app/ shares the root layout. If your dashboard needs a sidebar but your homepage doesn't, you end up with messy conditional logic:
1// app/layout.tsx — WITHOUT route groups 2export default function RootLayout({ children }) { 3 const pathname = usePathname(); 4 const isDashboard = pathname.startsWith("/dashboard"); 5 6 return ( 7 <html> 8 <body> 9 {isDashboard ? <Sidebar /> : <Navbar />} 10 {children} 11 {isDashboard ? null : <Footer />} 12 </body> 13 </html> 14 ); 15}
❌ Problem 2: Authentication Sprawl
You'd need to add auth checks to every single protected page individually:
1// app/dashboard/page.tsx 2export default function Dashboard() { 3 // Auth check repeated in dashboard, profile, settings... 4 const session = await getSession(); 5 if (!session) redirect("/login"); 6 return <div>Dashboard</div>; 7} 8 9// app/profile/page.tsx 10export default function Profile() { 11 // Same auth check again! 12 const session = await getSession(); 13 if (!session) redirect("/login"); 14 return <div>Profile</div>; 15}
✅ Solution: Route Groups
Route groups solve both problems elegantly:
- Different layouts per group (marketing layout vs. dashboard layout)
- Centralized auth at the group layout level
- Clean URLs without organizational folder names
3. Basic Syntax
Create a folder with parentheses (groupName):
app/
├── (public)/ ← Route group (no URL segment)
│ ├── layout.tsx ← Public layout (Navbar + Footer)
│ ├── page.tsx ← /
│ └── about/
│ └── page.tsx ← /about
│
└── (protected)/ ← Route group (no URL segment)
├── layout.tsx ← Protected layout (Sidebar + Auth check)
├── dashboard/
│ └── page.tsx → /dashboard
└── profile/
└── page.tsx → /profile
Key Rule: The group name is for developers, not users. Users never see it.
4. Simple Example
Public Layout
1// app/(public)/layout.tsx 2import { Navbar } from "@/components/layout/Navbar"; 3import { Footer } from "@/components/layout/Footer"; 4 5export default function PublicLayout({ 6 children, 7}: { 8 children: React.ReactNode; 9}) { 10 return ( 11 <> 12 <Navbar /> 13 <main className="min-h-screen">{children}</main> 14 <Footer /> 15 </> 16 ); 17}
Protected Layout with Auth
1// app/(protected)/layout.tsx 2import { redirect } from "next/navigation"; 3import { getSession } from "@/lib/auth/session"; 4import { Sidebar } from "@/components/layout/Sidebar"; 5 6export default async function ProtectedLayout({ 7 children, 8}: { 9 children: React.ReactNode; 10}) { 11 const session = await getSession(); 12 13 // Server-side auth check — runs BEFORE page renders 14 if (!session) { 15 redirect("/login"); 16 } 17 18 return ( 19 <div className="flex min-h-screen"> 20 <Sidebar user={session.user} /> 21 <main className="flex-1 p-8 bg-gray-50">{children}</main> 22 </div> 23 ); 24}
Dashboard Page
1// app/(protected)/dashboard/page.tsx 2export default function DashboardPage() { 3 // No auth check needed here — handled by parent layout! 4 return ( 5 <div> 6 <h1 className="text-3xl font-bold mb-6">Dashboard</h1> 7 <p>Welcome to your learning dashboard.</p> 8 </div> 9 ); 10}
5. Next.js Example: Complete Auth Architecture
Session Management (lib/auth/session.ts)
1import { cookies } from "next/headers"; 2import { jwtVerify } from "jose"; 3 4const secret = new TextEncoder().encode(process.env.JWT_SECRET); 5 6export async function getSession() { 7 const cookieStore = await cookies(); 8 const token = cookieStore.get("session")?.value; 9 10 if (!token) return null; 11 12 try { 13 const { payload } = await jwtVerify(token, secret); 14 return payload as { user: { id: string; email: string; name: string; role: string } }; 15 } catch { 16 return null; 17 } 18} 19 20export async function requireAuth() { 21 const session = await getSession(); 22 if (!session) { 23 redirect("/login"); 24 } 25 return session; 26} 27 28export async function requireRole(role: string) { 29 const session = await requireAuth(); 30 if (session.user.role !== role) { 31 redirect("/unauthorized"); 32 } 33 return session; 34}
Public Group Structure
1// app/(public)/layout.tsx 2import { Navbar } from "@/components/layout/Navbar"; 3import { Footer } from "@/components/layout/Footer"; 4 5export const metadata = { 6 title: "Tech3Space — Learn Tech Skills", 7}; 8 9export default function PublicLayout({ children }: { children: React.ReactNode }) { 10 return ( 11 <div className="flex flex-col min-h-screen"> 12 <Navbar /> 13 <main className="flex-grow">{children}</main> 14 <Footer /> 15 </div> 16 ); 17}
1// app/(public)/page.tsx 2export default function HomePage() { 3 return ( 4 <div className="text-center py-20"> 5 <h1 className="text-5xl font-bold mb-6">Master Modern Tech</h1> 6 <p className="text-xl text-gray-600 mb-8"> 7 Learn Next.js, React, Python & more with hands-on projects. 8 </p> 9 <a href="/courses" className="px-8 py-3 bg-blue-600 text-white rounded-lg"> 10 Explore Courses 11 </a> 12 </div> 13 ); 14}
1// app/(public)/courses/page.tsx 2export default function CoursesPage() { 3 return ( 4 <div className="max-w-6xl mx-auto px-4 py-12"> 5 <h1 className="text-3xl font-bold mb-8">All Courses</h1> 6 {/* Course grid */} 7 </div> 8 ); 9}
Protected Group Structure
1// app/(protected)/layout.tsx 2import { redirect } from "next/navigation"; 3import { getSession } from "@/lib/auth/session"; 4import { DashboardSidebar } from "@/components/layout/DashboardSidebar"; 5import { DashboardHeader } from "@/components/layout/DashboardHeader"; 6 7export default async function ProtectedLayout({ 8 children, 9}: { 10 children: React.ReactNode; 11}) { 12 const session = await getSession(); 13 14 if (!session) { 15 redirect("/login?callbackUrl=/dashboard"); 16 } 17 18 return ( 19 <div className="flex min-h-screen bg-gray-50"> 20 <DashboardSidebar user={session.user} /> 21 <div className="flex-1 flex flex-col"> 22 <DashboardHeader user={session.user} /> 23 <main className="flex-1 p-8">{children}</main> 24 </div> 25 </div> 26 ); 27}
1// app/(protected)/dashboard/page.tsx 2import { getEnrolledCourses } from "@/lib/api/courses"; 3import { getSession } from "@/lib/auth/session"; 4import { CourseProgressCard } from "@/components/dashboard/CourseProgressCard"; 5 6export default async function DashboardPage() { 7 const session = await getSession(); 8 const courses = await getEnrolledCourses(session!.user.id); 9 10 return ( 11 <div> 12 <h1 className="text-2xl font-bold mb-6">My Learning</h1> 13 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 14 {courses.map((course) => ( 15 <CourseProgressCard key={course.id} course={course} /> 16 ))} 17 </div> 18 </div> 19 ); 20}
1// app/(protected)/profile/page.tsx 2import { getSession } from "@/lib/auth/session"; 3import { ProfileForm } from "@/components/forms/ProfileForm"; 4 5export default async function ProfilePage() { 6 const session = await getSession(); 7 8 return ( 9 <div className="max-w-2xl"> 10 <h1 className="text-2xl font-bold mb-6">Profile Settings</h1> 11 <ProfileForm user={session!.user} /> 12 </div> 13 ); 14}
6. Real-World Example: Tech3Space Complete Auth Flow
Login Page (Public)
1// app/(public)/login/page.tsx 2import { LoginForm } from "@/components/forms/LoginForm"; 3 4export const metadata = { 5 title: "Login | Tech3Space", 6}; 7 8export default function LoginPage() { 9 return ( 10 <div className="min-h-screen flex items-center justify-center bg-gray-50"> 11 <div className="w-full max-w-md p-8 bg-white rounded-xl shadow-lg"> 12 <h1 className="text-2xl font-bold text-center mb-6"> 13 Welcome Back 14 </h1> 15 <LoginForm /> 16 <p className="text-center text-sm text-gray-500 mt-4"> 17 Don't have an account?{" "} 18 <a href="/register" className="text-blue-600 hover:underline"> 19 Sign up 20 </a> 21 </p> 22 </div> 23 </div> 24 ); 25}
Login Action (Server Action)
1// actions/auth.ts 2"use server"; 3 4import { redirect } from "next/navigation"; 5import { cookies } from "next/headers"; 6import { SignJWT } from "jose"; 7 8export async function login(formData: FormData) { 9 const email = formData.get("email") as string; 10 const password = formData.get("password") as string; 11 12 // Validate credentials (replace with real DB check) 13 const user = await validateCredentials(email, password); 14 15 if (!user) { 16 return { error: "Invalid email or password" }; 17 } 18 19 // Create JWT session 20 const token = await new SignJWT({ user }) 21 .setProtectedHeader({ alg: "HS256" }) 22 .setExpirationTime("7d") 23 .sign(new TextEncoder().encode(process.env.JWT_SECRET)); 24 25 // Set cookie 26 const cookieStore = await cookies(); 27 cookieStore.set("session", token, { 28 httpOnly: true, 29 secure: process.env.NODE_ENV === "production", 30 sameSite: "strict", 31 maxAge: 60 * 60 * 24 * 7, // 7 days 32 path: "/", 33 }); 34 35 redirect("/dashboard"); 36}
Logout Action
1// actions/auth.ts 2"use server"; 3 4import { cookies } from "next/headers"; 5import { redirect } from "next/navigation"; 6 7export async function logout() { 8 const cookieStore = await cookies(); 9 cookieStore.delete("session"); 10 redirect("/"); 11}
Dashboard Sidebar with Logout
1// components/layout/DashboardSidebar.tsx 2import Link from "next/link"; 3import { logout } from "@/actions/auth"; 4 5interface Props { 6 user: { name: string; email: string; role: string }; 7} 8 9export function DashboardSidebar({ user }: Props) { 10 const navItems = [ 11 { href: "/dashboard", label: "Dashboard", icon: "🏠" }, 12 { href: "/dashboard/courses", label: "My Courses", icon: "📚" }, 13 { href: "/dashboard/progress", label: "Progress", icon: "📊" }, 14 { href: "/profile", label: "Profile", icon: "👤" }, 15 { href: "/settings", label: "Settings", icon: "⚙️" }, 16 ]; 17 18 return ( 19 <aside className="w-64 bg-white border-r h-screen sticky top-0"> 20 <div className="p-6 border-b"> 21 <p className="font-semibold">{user.name}</p> 22 <p className="text-sm text-gray-500">{user.email}</p> 23 <span className="inline-block mt-2 px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded"> 24 {user.role} 25 </span> 26 </div> 27 <nav className="p-4 space-y-1"> 28 {navItems.map((item) => ( 29 <Link 30 key={item.href} 31 href={item.href} 32 className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 text-gray-700" 33 > 34 <span>{item.icon}</span> 35 {item.label} 36 </Link> 37 ))} 38 </nav> 39 <div className="p-4 mt-auto"> 40 <form action={logout}> 41 <button 42 type="submit" 43 className="w-full px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg text-sm" 44 > 45 Logout 46 </button> 47 </form> 48 </div> 49 </aside> 50 ); 51}
7. Common Mistakes
❌ Mistake 1: Thinking Route Groups Provide Security
1// WRONG — (protected) folder name does NOT protect anything 2// app/(protected)/dashboard/page.tsx 3export default function Dashboard() { 4 // Anyone can access this! The folder name is just for organization. 5 return <div>Secret Dashboard</div>; 6}
1// CORRECT — Always implement real auth checks 2// app/(protected)/layout.tsx 3export default async function ProtectedLayout({ children }) { 4 const session = await getSession(); 5 if (!session) redirect("/login"); // ✅ Real protection 6 return <div>{children}</div>; 7}
Critical: The
(protected)folder name is purely cosmetic. It does not enforce any security. You must implement authentication logic yourself.
❌ Mistake 2: Client-Side Auth Checks Only
1// WRONG — Client-side checks are bypassable 2"use client"; 3import { useEffect } from "react"; 4import { useRouter } from "next/navigation"; 5 6export default function Dashboard() { 7 const router = useRouter(); 8 9 useEffect(() => { 10 if (!localStorage.getItem("token")) { 11 router.push("/login"); // ❌ User sees the page flash first! 12 } 13 }, []); 14 15 return <div>Secret Data</div>; 16}
1// CORRECT — Server-side check blocks rendering entirely 2// app/(protected)/layout.tsx 3export default async function ProtectedLayout({ children }) { 4 const session = await getSession(); 5 if (!session) redirect("/login"); // ✅ Never renders if unauthorized 6 return <div>{children}</div>; 7}
❌ Mistake 3: Repeating Auth Logic in Every Page
1// WRONG — Duplicated across every protected page 2// app/(protected)/dashboard/page.tsx 3export default async function Dashboard() { 4 const session = await getSession(); 5 if (!session) redirect("/login"); 6 // ... 7} 8 9// app/(protected)/profile/page.tsx 10export default async function Profile() { 11 const session = await getSession(); 12 if (!session) redirect("/login"); 13 // ... 14} 15 16// app/(protected)/settings/page.tsx 17export default async function Settings() { 18 const session = await getSession(); 19 if (!session) redirect("/login"); 20 // ... 21}
1// CORRECT — One check in the group layout 2// app/(protected)/layout.tsx 3export default async function ProtectedLayout({ children }) { 4 const session = await getSession(); 5 if (!session) redirect("/login"); 6 return <div>{children}</div>; 7} 8 9// app/(protected)/dashboard/page.tsx 10export default async function Dashboard() { 11 // No auth check needed — handled by parent layout! 12 return <div>Dashboard</div>; 13}
❌ Mistake 4: Not Preserving Callback URLs
1// WRONG — User loses their intended destination 2redirect("/login");
1// CORRECT — Redirect back after login 2redirect(`/login?callbackUrl=${encodeURIComponent("/dashboard/settings")}`);
1// Login page reads callbackUrl 2// app/(public)/login/page.tsx 3export default async function LoginPage({ 4 searchParams, 5}: { 6 searchParams: Promise<{ callbackUrl?: string }>; 7}) { 8 const { callbackUrl } = await searchParams; 9 return <LoginForm redirectTo={callbackUrl || "/dashboard"} />; 10}
8. Best Practices
| Practice | Why It Matters |
|---|---|
| Server-side auth in layout | Blocks rendering before any data leaks |
| Use HTTP-only cookies | Prevents XSS token theft |
| JWT with expiration | Sessions expire automatically |
| Role-based layouts | Separate (admin) group for admin routes |
| Callback URLs | Users return to their intended page after login |
| Logout as Server Action | Properly clears cookies server-side |
| Parallel route groups | (marketing) + (app) for different experiences |
Role-Based Route Groups
app/
├── (public)/ ← Anyone can access
│ ├── page.tsx
│ ├── about/
│ └── login/
│
├── (student)/ ← Requires login
│ ├── dashboard/
│ ├── profile/
│ └── courses/
│
└── (admin)/ ← Requires admin role
├── admin/
│ ├── users/
│ ├── courses/
│ └── settings/
└── layout.tsx ← Checks role === "admin"
1// app/(admin)/layout.tsx 2import { requireRole } from "@/lib/auth/session"; 3 4export default async function AdminLayout({ children }: { children: React.ReactNode }) { 5 await requireRole("admin"); // ✅ Blocks non-admins 6 return ( 7 <div className="flex"> 8 <AdminSidebar /> 9 <main className="flex-1 p-8">{children}</main> 10 </div> 11 ); 12}
9. Architecture Explanation
Request Flow for Protected Routes
User visits: /dashboard
↓
Next.js matches: app/(protected)/dashboard/page.tsx
↓
Parent layout executes first: app/(protected)/layout.tsx
├── getSession() — reads JWT from HTTP-only cookie
├── Validate token signature & expiration
├── If invalid → redirect("/login?callbackUrl=/dashboard")
└── If valid → render layout with user data
↓
Dashboard page executes: app/(protected)/dashboard/page.tsx
├── Already authenticated (guaranteed by layout)
├── Fetch user-specific data (enrolled courses)
└── Render dashboard
↓
Fully rendered HTML sent to browser (no flash of unauthenticated content)
Security Architecture Comparison
| Approach | Security | UX | Performance |
|---|---|---|---|
Client-side redirect (useEffect) | ❌ Bypassable | ⚠️ Flash of content | ⚠️ Delayed |
| Middleware check | ✅ Good | ✅ Clean | ✅ Fast |
| Server layout check | ✅ Best | ✅ Clean | ✅ Fastest |
| API route check only | ❌ Page still renders | ❌ Confusing | ❌ Wasteful |
Recommendation: Use server layout checks for page-level protection and middleware for API route protection.
10. Practice Task
Task: Build a multi-role application with route groups.
Requirements:
-
Create three route groups:
(public)— Home, About, Login, Register(student)— Dashboard, My Courses, Progress(admin)— Admin Dashboard, User Management, Course Management
-
Implement server-side auth in each protected layout
-
Create a
requireRole(role)utility function -
Add a logout button that works via Server Action
-
Preserve
callbackUrlwhen redirecting to login
File structure to build:
app/
├── (public)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ ├── login/
│ └── register/
├── (student)/
│ ├── layout.tsx
│ ├── dashboard/
│ ├── my-courses/
│ └── progress/
└── (admin)/
├── layout.tsx
├── admin/
│ ├── users/
│ └── courses/
11. Mini Project: Tech3Space Auth System
Build a complete authentication architecture for Tech3Space:
File Structure
app/
├── (public)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── courses/
│ │ └── page.tsx
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
│
├── (protected)/
│ ├── layout.tsx
│ ├── dashboard/
│ │ └── page.tsx
│ ├── profile/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
│
├── (admin)/
│ ├── layout.tsx
│ └── admin/
│ ├── page.tsx
│ ├── users/
│ │ └── page.tsx
│ └── courses/
│ └── page.tsx
│
├── layout.tsx
└── api/
└── auth/
└── route.ts
app/(protected)/layout.tsx
1import { redirect } from "next/navigation"; 2import { getSession } from "@/lib/auth/session"; 3import { DashboardSidebar } from "@/components/layout/DashboardSidebar"; 4import { DashboardHeader } from "@/components/layout/DashboardHeader"; 5 6export default async function ProtectedLayout({ 7 children, 8}: { 9 children: React.ReactNode; 10}) { 11 const session = await getSession(); 12 13 if (!session) { 14 const callbackUrl = encodeURIComponent("/dashboard"); 15 redirect(`/login?callbackUrl=${callbackUrl}`); 16 } 17 18 return ( 19 <div className="flex min-h-screen bg-gray-50"> 20 <DashboardSidebar user={session.user} /> 21 <div className="flex-1 flex flex-col min-w-0"> 22 <DashboardHeader user={session.user} /> 23 <main className="flex-1 p-8 overflow-auto">{children}</main> 24 </div> 25 </div> 26 ); 27}
app/(admin)/layout.tsx
1import { redirect } from "next/navigation"; 2import { requireRole } from "@/lib/auth/session"; 3import { AdminSidebar } from "@/components/layout/AdminSidebar"; 4 5export default async function AdminLayout({ 6 children, 7}: { 8 children: React.ReactNode; 9}) { 10 try { 11 const session = await requireRole("admin"); 12 return ( 13 <div className="flex min-h-screen bg-gray-900 text-white"> 14 <AdminSidebar user={session.user} /> 15 <main className="flex-1 p-8">{children}</main> 16 </div> 17 ); 18 } catch { 19 redirect("/unauthorized"); 20 } 21}
app/(public)/login/page.tsx
1import { redirect } from "next/navigation"; 2import { getSession } from "@/lib/auth/session"; 3import { LoginForm } from "@/components/forms/LoginForm"; 4 5export default async function LoginPage({ 6 searchParams, 7}: { 8 searchParams: Promise<{ callbackUrl?: string }>; 9}) { 10 const session = await getSession(); 11 const { callbackUrl } = await searchParams; 12 13 // Already logged in? Redirect to dashboard 14 if (session) { 15 redirect(callbackUrl || "/dashboard"); 16 } 17 18 return ( 19 <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4"> 20 <div className="w-full max-w-md"> 21 <div className="bg-white rounded-2xl shadow-xl p-8"> 22 <h1 className="text-2xl font-bold text-center mb-2"> 23 Welcome to Tech3Space 24 </h1> 25 <p className="text-gray-500 text-center mb-8"> 26 Sign in to continue learning 27 </p> 28 <LoginForm redirectTo={callbackUrl || "/dashboard"} /> 29 </div> 30 </div> 31 </div> 32 ); 33}
Summary
| Concept | Purpose | URL Impact |
|---|---|---|
| Route Group | Organize routes logically | None — invisible in URL |
(public) | Marketing pages, auth pages | /, /about, /login |
(protected) | Authenticated user pages | /dashboard, /profile |
(admin) | Admin-only pages | /admin/users |
| Layout auth | Centralized protection | Runs before any page renders |
| Server redirect | Secure navigation | No content flash |
| Callback URL | Return after login | Preserves user intent |
What's Next?
In Module 9, we'll cover React Fundamentals Required for Next.js — components, props, state, conditional rendering, lists, and events. These are the building blocks you need before diving deeper into Server and Client Components.
Your mission: Implement route groups in your Tech3Space app. Create (public) and (protected) groups with different layouts. Add a real server-side auth check in the protected layout. Test that unauthenticated users are redirected to login, and that the callback URL returns them to their intended destination after signing in!