Module 21 — Protected Application Architecture
A production Next.js application usually contains both public and protected areas.
For example, a course platform may have:
1Public 2├── Home 3├── Courses 4├── Course Details 5└── Login 6 7Protected 8├── Dashboard 9├── Profile 10├── Settings 11├── My Courses 12└── Admin
A clean Next.js project should organize these areas clearly.
However, there is one extremely important concept:
Folder organization is not security.
A folder named (protected) does not automatically prevent users from accessing the routes inside it.
Public and Protected Routes
A typical application can be structured like this:
1app/ 2├── (public)/ 3│ ├── page.tsx 4│ ├── courses/ 5│ │ ├── page.tsx 6│ │ └── [slug]/ 7│ │ └── page.tsx 8│ │ 9│ └── login/ 10│ └── page.tsx 11│ 12└── (protected)/ 13 ├── dashboard/ 14 │ └── page.tsx 15 │ 16 ├── profile/ 17 │ └── page.tsx 18 │ 19 ├── courses/ 20 │ └── page.tsx 21 │ 22 └── settings/ 23 └── page.tsx
This creates a clean project structure.
But there is an important detail about the folder names.
What Are Route Groups?
The parentheses in:
1(public) 2(protected)
indicate route groups in the Next.js App Router.
For example:
1app/ 2└── (public)/ 3 └── courses/ 4 └── page.tsx
The (public) segment does not become part of the URL.
Therefore:
1app/(public)/courses/page.tsx
produces:
1/courses
not:
1/(public)/courses
Similarly:
1app/(protected)/dashboard/page.tsx
produces:
1/dashboard
not:
1/(protected)/dashboard
Why Route Groups Are Useful
Route groups help organize large applications without changing URLs.
For example:
1app/ 2├── (public)/ 3├── (protected)/ 4├── (auth)/ 5└── (admin)/
You can separate application areas logically.
A large application might look like:
1app/ 2├── (public)/ 3│ ├── page.tsx 4│ ├── courses/ 5│ └── pricing/ 6│ 7├── (auth)/ 8│ ├── login/ 9│ ├── register/ 10│ └── forgot-password/ 11│ 12├── (protected)/ 13│ ├── dashboard/ 14│ ├── profile/ 15│ └── settings/ 16│ 17└── (admin)/ 18 └── admin/ 19 ├── users/ 20 └── courses/
This makes the project easier to understand.
Route Organization
Route organization answers:
Where should this code live?
For example:
1app/ 2└── (protected)/ 3 ├── dashboard/ 4 ├── profile/ 5 └── settings/
This tells developers:
1dashboard → protected area 2profile → protected area 3settings → protected area
But this is only an organizational convention.
It does not enforce authentication.
Actual Security
Actual security answers:
Is the current user allowed to access this resource?
The server must perform checks such as:
1Request 2 ↓ 3Is user authenticated? 4 ↓ 5Is user authorized? 6 ↓ 7Allow or reject
For example:
1GET /dashboard 2 ↓ 3Check session 4 ↓ 5User authenticated? 6 ↙ ↘ 7 No Yes 8 ↓ ↓ 9Redirect Continue 10/login Dashboard
The Important Difference
Think of it this way:
1Route Group 2 ↓ 3Organization
while:
1Authentication / Authorization 2 ↓ 3Security
Therefore:
1(protected)
is a developer convention, not an authentication mechanism.
A Common Misunderstanding
A developer might create:
1app/ 2└── (protected)/ 3 └── dashboard/ 4 └── page.tsx
and assume:
"Because the dashboard is inside
(protected), Next.js will require authentication."
That assumption is incorrect.
The route is still:
1/dashboard
and Next.js does not automatically know that (protected) means:
1"Only authenticated users may enter."
You must implement the security logic yourself or use an authentication system that provides it.
Protecting a Page
A simple server-side approach is:
1import { redirect } from "next/navigation"; 2import { getCurrentUser } from "@/lib/auth"; 3 4export default async function DashboardPage() { 5 const user = await getCurrentUser(); 6 7 if (!user) { 8 redirect("/login"); 9 } 10 11 return ( 12 <main> 13 <h1>Dashboard</h1> 14 15 <p> 16 Welcome, {user.name} 17 </p> 18 </main> 19 ); 20}
The important part is:
1const user = await getCurrentUser(); 2 3if (!user) { 4 redirect("/login"); 5}
Now the server actually enforces authentication.
Protected Layout
Instead of repeating the same authentication check on every page, you can use a protected layout.
For example:
1app/ 2└── (protected)/ 3 ├── layout.tsx 4 ├── dashboard/ 5 │ └── page.tsx 6 ├── profile/ 7 │ └── page.tsx 8 └── settings/ 9 └── page.tsx
The layout can perform the authentication check.
1import { redirect } from "next/navigation"; 2import { getCurrentUser } from "@/lib/auth"; 3 4export default async function ProtectedLayout({ 5 children, 6}: { 7 children: React.ReactNode; 8}) { 9 const user = await getCurrentUser(); 10 11 if (!user) { 12 redirect("/login"); 13 } 14 15 return ( 16 <div> 17 {children} 18 </div> 19 ); 20}
Now the architecture becomes:
1Request 2 ↓ 3Protected Layout 4 ↓ 5Authentication Check 6 ↓ 7Authenticated? 8 ↙ ↘ 9No Yes 10↓ ↓ 11/login Page
This can centralize protection for an entire section.
Protected Layout Architecture
A practical structure:
1app/ 2├── (public)/ 3│ ├── page.tsx 4│ └── courses/ 5│ 6└── (protected)/ 7 ├── layout.tsx 8 │ 9 ├── dashboard/ 10 │ └── page.tsx 11 │ 12 ├── profile/ 13 │ └── page.tsx 14 │ 15 ├── courses/ 16 │ └── page.tsx 17 │ 18 └── settings/ 19 └── page.tsx
The layout becomes the common authentication boundary.
Public Routes
Public routes can be accessed without authentication.
For example:
1/ 2 /courses 3 /courses/nextjs 4 /pricing 5 /about 6 /login 7 /register
Architecture:
1User 2 ↓ 3Public Route 4 ↓ 5Page
No login is required.
Protected Routes
Protected routes require authentication.
For example:
1/dashboard 2/profile 3/settings 4/my-courses
Architecture:
1User 2 ↓ 3Protected Route 4 ↓ 5Authentication 6 ↓ 7Authenticated? 8 ↓ 9Page
Admin Routes
Some routes require more than authentication.
For example:
1/admin 2/admin/users 3/admin/courses 4/admin/settings
The architecture becomes:
1Request 2 ↓ 3Authentication 4 ↓ 5User exists? 6 ↓ 7Authorization 8 ↓ 9Admin? 10 ↓ 11Allow
Authentication alone is not enough.
Authentication vs Authorization
Remember:
1Authentication 2= 3Who is the user?
while:
1Authorization 2= 3What is the user allowed to do?
For example:
1User 2 ↓ 3Logged in 4 ↓ 5Authenticated 6 ↓ 7Role = student 8 ↓ 9Not authorized for /admin
The user is authenticated but not authorized.
Admin Layout
You can create another logical route group:
1app/ 2└── (admin)/ 3 └── admin/ 4 ├── layout.tsx 5 ├── page.tsx 6 ├── users/ 7 └── courses/
The admin layout can enforce authorization.
1import { redirect } from "next/navigation"; 2import { getCurrentUser } from "@/lib/auth"; 3 4export default async function AdminLayout({ 5 children, 6}: { 7 children: React.ReactNode; 8}) { 9 const user = await getCurrentUser(); 10 11 if (!user) { 12 redirect("/login"); 13 } 14 15 if (user.role !== "admin") { 16 redirect("/forbidden"); 17 } 18 19 return ( 20 <div> 21 {children} 22 </div> 23 ); 24}
Now:
1/admin 2/admin/users 3/admin/courses
can share the same authorization boundary.
Nested Security Boundaries
Large applications can have multiple security levels.
For example:
1Application 2│ 3├── Public 4│ 5├── Authenticated 6│ ├── Dashboard 7│ ├── Profile 8│ └── Settings 9│ 10└── Admin 11 ├── Users 12 ├── Courses 13 └── Settings
The security model becomes:
1Public 2 ↓ 3No authentication required 4 5Authenticated 6 ↓ 7Login required 8 9Admin 10 ↓ 11Login + admin permission required
This is a powerful architecture for large applications.
Route Organization vs Security
Let's compare them directly.
| Concept | Route Organization | Actual Security |
|---|---|---|
| Purpose | Organize project | Protect resources |
| Implemented by | Folders/layouts | Server logic |
(protected) | Yes | No |
| Authentication check | No | Yes |
| Authorization check | No | Yes |
| URL changes | Route groups don't | Depends on implementation |
| Prevents unauthorized access | No | Yes |
| Developer experience | Excellent | Essential |
The key distinction is:
1Route Group 2 ↓ 3Structure 4 5Authentication 6 ↓ 7Security
Protecting APIs
A protected page is not enough.
Suppose you have:
1/dashboard
and:
1/api/courses
The API must also verify authentication.
For example:
1import { NextResponse } from "next/server"; 2import { getCurrentUser } from "@/lib/auth"; 3 4export async function GET() { 5 const user = await getCurrentUser(); 6 7 if (!user) { 8 return NextResponse.json( 9 { 10 error: "Authentication required", 11 }, 12 { 13 status: 401, 14 } 15 ); 16 } 17 18 return NextResponse.json({ 19 courses: [], 20 }); 21}
The architecture is:
1API Request 2 ↓ 3Authentication 4 ↓ 5Authorized? 6 ↓ 7Business Logic 8 ↓ 9Database
Why Page Protection Alone Is Dangerous
Imagine:
1/dashboard
is protected.
But:
1/api/user/profile
is not.
A malicious user could bypass the dashboard and call the API directly.
Therefore:
1Protected Page 2 + 3Protected API 4 + 5Protected Server Action 6 + 7Protected Database Mutation
are all important.
Server Actions Must Also Be Protected
Suppose you have:
1async function deleteCourse( 2 courseId: string 3) { 4 "use server"; 5 6 // Delete course 7}
Do not assume that because the function is called from an admin page, it is automatically safe.
The server action should verify:
1User authenticated? 2 ↓ 3User authorized? 4 ↓ 5Course exists? 6 ↓ 7User allowed to delete? 8 ↓ 9Delete
For example:
1async function deleteCourse( 2 courseId: string 3) { 4 "use server"; 5 6 const user = 7 await getCurrentUser(); 8 9 if (!user) { 10 throw new Error( 11 "Authentication required" 12 ); 13 } 14 15 if (user.role !== "admin") { 16 throw new Error( 17 "Forbidden" 18 ); 19 } 20 21 // Delete course 22}
Resource-Level Authorization
Role checking alone is sometimes insufficient.
Suppose an instructor owns course 123.
Another instructor should not necessarily be able to modify it.
The server might check:
1Authenticated? 2 ↓ 3Instructor? 4 ↓ 5Does this instructor own course? 6 ↓ 7Allow update
Conceptually:
1const course = 2 await getCourse(courseId); 3 4if ( 5 course.instructorId !== user.id 6) { 7 throw new Error( 8 "Forbidden" 9 ); 10}
This is called resource-level authorization.
Three Levels of Protection
A useful mental model is:
1Level 1 2Authentication 3↓ 4Is the user logged in? 5 6Level 2 7Authorization 8↓ 9Does the user have this permission? 10 11Level 3 12Resource Authorization 13↓ 14Can this user modify this specific resource?
Example:
1Student 2 ↓ 3Authenticated ✓ 4 ↓ 5Admin permission ✗
or:
1Instructor 2 ↓ 3Authenticated ✓ 4 ↓ 5Course editing permission ✓ 6 ↓ 7Owns this course? ✗ 8 ↓ 9Reject
Middleware and Protection
Next.js applications can also use middleware/proxy-style request interception depending on the Next.js version and architecture.
Conceptually:
1Request 2 ↓ 3Request Interceptor 4 ↓ 5Authentication Check 6 ↓ 7Route
For example:
1/dashboard 2/admin/*
can be identified as protected paths.
However, request interception should not become your only security layer for sensitive operations.
A good architecture is:
1Request Layer 2 ↓ 3Early access decision 4 ↓ 5Server Page / API / Action 6 ↓ 7Authorization 8 ↓ 9Business Logic
The actual security checks should remain close to the protected operation.
Do Not Rely Only on Middleware
A common mistake is:
1Middleware 2 ↓ 3Protected
and assuming every internal operation is therefore secure.
But applications can have:
1Server Actions 2API Routes 3Database Functions 4Background Jobs 5Internal Services
Security should be enforced at the appropriate server-side boundary.
Example: Course Platform
Consider a real course platform:
1app/ 2├── (public)/ 3│ ├── page.tsx 4│ ├── courses/ 5│ ├── pricing/ 6│ └── login/ 7│ 8├── (protected)/ 9│ ├── layout.tsx 10│ ├── dashboard/ 11│ ├── profile/ 12│ ├── my-courses/ 13│ └── settings/ 14│ 15└── (admin)/ 16 └── admin/ 17 ├── layout.tsx 18 ├── users/ 19 ├── courses/ 20 └── analytics/
Security:
1Public 2 ↓ 3No authentication 4 5Protected 6 ↓ 7Authentication required 8 9Admin 10 ↓ 11Authentication 12 ↓ 13Admin authorization
Recommended Project Structure
For a medium-sized application:
1app/ 2├── (public)/ 3│ ├── page.tsx 4│ ├── courses/ 5│ ├── pricing/ 6│ └── login/ 7│ 8├── (protected)/ 9│ ├── layout.tsx 10│ ├── dashboard/ 11│ ├── profile/ 12│ ├── my-courses/ 13│ └── settings/ 14│ 15├── (admin)/ 16│ └── admin/ 17│ ├── layout.tsx 18│ ├── users/ 19│ ├── courses/ 20│ └── analytics/ 21│ 22└── api/ 23 ├── auth/ 24 ├── courses/ 25 └── users/ 26 27lib/ 28├── auth/ 29│ ├── session.ts 30│ ├── user.ts 31│ └── permissions.ts 32│ 33├── api/ 34│ ├── courses.ts 35│ └── users.ts 36│ 37└── db/ 38 └── index.ts
This gives you a clear separation between:
1Routes 2Authentication 3Authorization 4API Services 5Database
Route Groups Do Not Change URLs
This is worth remembering.
Given:
1app/ 2├── (public)/ 3│ └── courses/ 4│ └── page.tsx 5│ 6└── (protected)/ 7 └── dashboard/ 8 └── page.tsx
the URLs remain:
1/courses 2/dashboard
The group names:
1(public) 2(protected)
exist for project organization.
They are not URL segments.
Shared Layouts
Route groups can also help you create different layouts.
For example:
1app/ 2├── (public)/ 3│ └── layout.tsx 4│ 5├── (protected)/ 6│ └── layout.tsx 7│ 8└── (admin)/ 9 └── layout.tsx
You could have:
1Public Layout 2 ↓ 3Marketing Navigation 4 ↓ 5Public Pages
while:
1Protected Layout 2 ↓ 3Application Sidebar 4 ↓ 5Dashboard Pages
and:
1Admin Layout 2 ↓ 3Admin Navigation 4 ↓ 5Admin Pages
This is one of the strongest reasons to use route groups.
Security Boundary vs UI Boundary
A protected layout can serve both purposes:
1Protected Layout 2├── UI organization 3├── Shared navigation 4└── Authentication boundary
But the folder itself does not provide the security.
Think:
1(protected) 2 ↓ 3Logical boundary 4 5ProtectedLayout 6 ↓ 7Authentication enforcement
A Better Architecture
The complete architecture can be represented as:
1 NEXT.JS APP 2 │ 3 ┌────────────────┼────────────────┐ 4 │ │ │ 5 ▼ ▼ ▼ 6 PUBLIC PROTECTED ADMIN 7 │ │ │ 8 │ ▼ ▼ 9 │ Authentication Authentication 10 │ │ │ 11 │ ▼ ▼ 12 │ Authorization Admin Permission 13 │ │ │ 14 └────────────────┼────────────────┘ 15 ▼ 16 Server Operations 17 │ 18 ┌─────────┴─────────┐ 19 ▼ ▼ 20 API Server Action 21 │ │ 22 └─────────┬─────────┘ 23 ▼ 24 Database
This is much closer to a production architecture.
Common Mistakes
Mistake 1 — Assuming (protected) Provides Security
Wrong:
1app/(protected)/dashboard/page.tsx
does not automatically authenticate the user.
Correct:
1Protected route 2 ↓ 3Server authentication check 4 ↓ 5Allow / redirect
Mistake 2 — Protecting Only Pages
Wrong:
1/dashboard → protected 2/api/courses → public
The API may still expose sensitive operations.
Protect both.
Mistake 3 — Hiding Admin UI
This:
1{user.role === "admin" && ( 2 <DeleteButton /> 3)}
is useful UI logic.
But it is not sufficient authorization.
The server must enforce:
1user.role === "admin"
before deletion.
Mistake 4 — Trusting Client-Supplied Roles
Never trust:
1{ 2 "role": "admin" 3}
from the browser.
Determine identity and permissions from trusted server-side authentication state.
Mistake 5 — Checking Authentication Too Late
For sensitive pages and operations, perform authentication and authorization before executing protected business logic.
Module 21 Learning Checklist
After completing this module, you should understand:
- Public routes
- Protected routes
- Route groups
(public)route groups(protected)route groups(admin)route groups- Nested layouts
- Protected layouts
- Authentication boundaries
- Authorization boundaries
- Resource-level authorization
- Protected APIs
- Protected Server Actions
- Middleware/request interception
- Route organization
- Actual application security
- Why route groups do not provide authentication
- Why hiding UI is not security
- Why APIs must be protected independently
- How to structure a production Next.js application
Final Mental Model
Remember this distinction:
1 ROUTE ORGANIZATION 2 │ 3 ▼ 4 (public) / (protected) 5 │ 6 ▼ 7 Project Structure
versus:
1 ACTUAL SECURITY 2 │ 3 ▼ 4 Authentication 5 │ 6 ▼ 7 Authorization 8 │ 9 ▼ 10 Server Page / API / Action 11 │ 12 ▼ 13 Database
The complete flow is:
1User 2 ↓ 3Request 4 ↓ 5Route Organization 6 ↓ 7Authentication 8 ↓ 9Authorization 10 ↓ 11Business Logic 12 ↓ 13Database 14 ↓ 15Response
A
(protected)folder tells developers that a route is intended to be protected; it does not protect the route. Real security comes from server-side authentication and authorization checks.