Dev Logs
/Next.js/ Chapter 6: Route Groups & Organization
Chapters
  • 01Chapter 1: Introduction & Setup
  • 02Chapter 2: Project Structure & Configuration
  • 03Chapter 3: Layouts & Pages
  • 04Chapter 4: Linking & Navigation
  • 05Chapter 5: Dynamic Routes & Params
  • 06Chapter 6: Route Groups & Organization
    • Plain English Explanation
    • The three jobs route groups do
    • Job 1: Different layouts at the same URL level
    • Job 2: Organizing without any layout change
    • Job 3: Opting a section out of a layout
    • Multiple root layouts
    • The cost
    • Route groups + dynamic root segments
    • A realistic full structure
    • Common Pitfalls
    • . Two groups producing the same URL
    • . Root layout plus group root layouts
    • . Expecting shared state across root layouts
    • . Assuming the group name affects the URL
    • . Treating a layout auth check as security
    • . Over-grouping
    • When & Why to Use
    • Mini Practice Problems
    • Problem 1: URL mapping
    • Problem 2: Fix the conflict
    • Problem 3: Design it
    • Problem 4: Predict the cost
    • Interview Notes
    • Common Questions
    • Asked at Companies
    • Visual Memory Aid
    • Key Takeaways
  • 07Chapter 7: Parallel & Intercepting Routes
  • 08Chapter 8: Loading, Suspense & Streaming
  • 09Chapter 9: Error Handling
  • 10Chapter 10: Server & Client Components
  • 11Chapter 11: Data Fetching
  • 12Chapter 12: Server Actions & Mutations
  • 13Chapter 13: Route Handlers
  • 14Chapter 14: Caching & use cache
  • 15Chapter 15: Revalidation & ISR
  • 16Chapter 16: Cache Components & Partial Prerendering
  • 17Chapter 17: Proxy (formerly Middleware)
  • 18Chapter 18: Authentication & Authorization
  • 19Chapter 19: Metadata, SEO & OG Images
  • 20Chapter 20: Images & Fonts
  • 21Chapter 21: Styling
  • 22Chapter 22: Performance Optimization
  • 23Chapter 23: Testing
  • 24Chapter 24: Deployment & Self-Hosting
  • 25Chapter 25: Upgrading to Next.js 16
  • 26Chapter 26: Capstone Project
  • 27Chapter 27: React Performance Profiling
All chapters

🗃️ Chapter 6: Route Groups & Organization

Giving different sections of your app completely different shells, without those sections showing up in the URL.

📖 Plain English Explanation

Here's a problem every real app runs into.

Your marketing pages (/, /pricing, /about) need a public header with a "Sign up" button and a big footer with links to your social media.

Your app pages (/dashboard, /settings, /billing) need a sidebar, a user menu, and no footer at all.

Both live at the top level of your URL structure. You can't put the dashboard pages inside an app/dashboard/ folder because then they'd be at /dashboard/settings instead of /settings. And layouts follow folders, so if the folders are flat, the layouts have to be too.

Route groups solve exactly this. Wrap a folder name in parentheses and it organizes your files without appearing in the URL:

app/
├── (marketing)/
│   ├── layout.tsx          ← public header + footer
│   ├── page.tsx            →  /
│   └── pricing/page.tsx    →  /pricing
└── (app)/
    ├── layout.tsx          ← sidebar + user menu
    ├── dashboard/page.tsx  →  /dashboard
    └── settings/page.tsx   →  /settings

Two completely separate shells. The URLs stay clean. Nobody visiting your site ever sees the word "marketing".

🎯 The three jobs route groups do

Job 1: Different layouts at the same URL level

This is the main use, shown above. The key insight: a route group creates a new layout boundary without creating a URL boundary.

tsx
// app/(marketing)/layout.tsx
import { PublicHeader } from '@/components/PublicHeader'
import { Footer } from '@/components/Footer'

export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      <PublicHeader />
      <main className="mx-auto max-w-5xl px-6">{children}</main>
      <Footer />
    </>
  )
}
jsx
// app/(marketing)/layout.js
import { PublicHeader } from '@/components/PublicHeader'
import { Footer } from '@/components/Footer'

export default function MarketingLayout({ children }) {
  return (
    <>
      <PublicHeader />
      <main className="mx-auto max-w-5xl px-6">{children}</main>
      <Footer />
    </>
  )
}
tsx
// app/(app)/layout.tsx
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { Sidebar } from '@/components/Sidebar'

export default async function AppLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await getSession()
  if (!session) redirect('/login')

  return (
    <div className="flex min-h-screen">
      <Sidebar user={session.user} />
      <div className="flex-1 p-8">{children}</div>
    </div>
  )
}
jsx
// app/(app)/layout.js
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { Sidebar } from '@/components/Sidebar'

export default async function AppLayout({ children }) {
  const session = await getSession()
  if (!session) redirect('/login')

  return (
    <div className="flex min-h-screen">
      <Sidebar user={session.user} />
      <div className="flex-1 p-8">{children}</div>
    </div>
  )
}

Notice the auth check in the (app) layout. Every route in that group is now gated in one place. That's a genuinely useful side effect of grouping by shell.

Security note: a layout auth check is a convenience, not a security boundary. Layouts don't re-run on every navigation, and Server Actions and Route Handlers bypass layouts entirely. Chapter 18 covers where the real check belongs.

Job 2: Organizing without any layout change

Sometimes you just want related routes near each other:

app/
├── (legal)/
│   ├── privacy/page.tsx     →  /privacy
│   ├── terms/page.tsx       →  /terms
│   └── cookies/page.tsx     →  /cookies
└── (support)/
    ├── help/page.tsx        →  /help
    └── contact/page.tsx     →  /contact

No layout.tsx in either group — they just inherit the root layout. The parentheses are purely for humans reading the file tree.

Job 3: Opting a section out of a layout

A route group can escape a layout that would otherwise apply. Compare:

app/
├── layout.tsx                    ← root
├── (shop)/
│   ├── layout.tsx                ← shop chrome
│   ├── products/page.tsx         →  /products   (has shop chrome)
│   └── cart/page.tsx             →  /cart       (has shop chrome)
└── checkout/page.tsx             →  /checkout   (root layout only — no chrome)

/checkout sits outside (shop), so it gets a bare page with no shop navigation — exactly what you want for a distraction-free checkout flow.

🏛️ Multiple root layouts

You can go further and give groups their own <html> and <body>. This creates genuinely separate applications sharing one codebase.

The rule: delete app/layout.tsx entirely, and give each top-level group its own root layout.

app/
├── (marketing)/
│   ├── layout.tsx          ← has <html> and <body>
│   ├── page.tsx            →  /
│   └── pricing/page.tsx    →  /pricing
└── (app)/
    ├── layout.tsx          ← has its own <html> and <body>
    └── dashboard/page.tsx  →  /dashboard
tsx
// app/(marketing)/layout.tsx
import { Playfair_Display } from 'next/font/google'
import './marketing.css'

const playfair = Playfair_Display({ subsets: ['latin'] })

export const metadata = {
  title: 'Acme — Widgets for everyone',
}

export default function MarketingRootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={playfair.className}>
      <body className="bg-white">{children}</body>
    </html>
  )
}
tsx
// app/(app)/layout.tsx
import { Inter } from 'next/font/google'
import './app.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Acme Dashboard',
}

export default function AppRootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.className}>
      <body className="bg-slate-50">{children}</body>
    </html>
  )
}
jsx
// app/(app)/layout.js
import { Inter } from 'next/font/google'
import './app.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Acme Dashboard',
}

export default function AppRootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body className="bg-slate-50">{children}</body>
    </html>
  )
}

Different fonts, different global CSS, different <body> classes. Nothing shared but the codebase.

The cost

Navigating between root layouts is a full page reload. Going from /pricing to /dashboard tears down the document and starts fresh — the white flash is back.

That is usually fine. Users cross from marketing to app once per session, and a hard reload at that boundary is invisible in practice. But if two sections are navigated between constantly, do not split their root layouts.

Use multiple root layouts whenUse one root layout when
Sections have different fonts / global CSSSections share a design system
Users cross the boundary rarelyUsers cross constantly
You want fully separate <head> setupsA nested layout is enough

🌐 Route groups + dynamic root segments

A common real-world combination: internationalization with a group per audience.

app/
└── [locale]/
    ├── layout.tsx                  ← sets <html lang>, loads translations
    ├── (marketing)/
    │   ├── layout.tsx
    │   ├── page.tsx                →  /en, /fr
    │   └── pricing/page.tsx        →  /en/pricing, /fr/pricing
    └── (app)/
        ├── layout.tsx
        └── dashboard/page.tsx      →  /en/dashboard, /fr/dashboard
tsx
// app/[locale]/layout.tsx
export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'fr' }, { locale: 'de' }]
}

export default async function LocaleLayout(props: LayoutProps<'/[locale]'>) {
  const { locale } = await props.params
  return (
    <html lang={locale}>
      <body>{props.children}</body>
    </html>
  )
}
jsx
// app/[locale]/layout.js
export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'fr' }, { locale: 'de' }]
}

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  )
}

Components deep inside can read the locale without prop drilling, using next/root-params from Chapter 5:

ts
// lib/t.ts
import { locale } from 'next/root-params'

export async function t(key: string) {
  const current = await locale()
  const dict = await import(`@/messages/${current}.json`)
  return dict.default[key] ?? key
}

🧱 A realistic full structure

Here's what a mature app looks like with everything from Chapters 2 and 6 combined:

app/
├── layout.tsx                       ← root: <html>, <body>, providers
├── globals.css
├── not-found.tsx                    ← global 404
│
├── (marketing)/
│   ├── layout.tsx                   ← public header + footer
│   ├── page.tsx                     →  /
│   ├── pricing/page.tsx             →  /pricing
│   ├── blog/
│   │   ├── page.tsx                 →  /blog
│   │   └── [slug]/page.tsx          →  /blog/:slug
│   └── _components/
│       └── PricingTable.tsx         ← private, never routed
│
├── (auth)/
│   ├── layout.tsx                   ← centered card, no nav
│   ├── login/page.tsx               →  /login
│   ├── register/page.tsx            →  /register
│   └── forgot-password/page.tsx     →  /forgot-password
│
├── (app)/
│   ├── layout.tsx                   ← auth gate + sidebar
│   ├── dashboard/
│   │   ├── page.tsx                 →  /dashboard
│   │   ├── loading.tsx
│   │   └── error.tsx
│   ├── settings/
│   │   ├── layout.tsx               ← settings sub-nav
│   │   ├── page.tsx                 →  /settings
│   │   └── billing/page.tsx         →  /settings/billing
│   └── _components/
│       └── Sidebar.tsx
│
├── api/
│   └── webhooks/
│       └── stripe/route.ts          →  /api/webhooks/stripe
│
└── proxy.ts                         ← runs before requests

Read that tree top to bottom and you understand the whole application. That's the goal.

⚠️ Common Pitfalls

1. Two groups producing the same URL

app/
├── (marketing)/about/page.tsx   →  /about
└── (legal)/about/page.tsx       →  /about   ❌ conflict
Error: You cannot have two parallel pages that resolve to the same path

Parentheses are invisible to the router, so both really are /about. Fix: rename one route.

The subtle version:

app/
├── (marketing)/page.tsx    →  /
└── (app)/page.tsx          →  /   ❌ same conflict

Only one group can own the index route.

2. Root layout plus group root layouts

app/
├── layout.tsx              ← has <html>
├── (marketing)/layout.tsx  ← also has <html>  ❌ nested <html>
└── (app)/layout.tsx        ← also has <html>  ❌

If you want multiple root layouts, app/layout.tsx must not exist. It's all-or-nothing.

3. Expecting shared state across root layouts

tsx
// app/(marketing)/layout.tsx
<CartProvider>{children}</CartProvider>

// app/(app)/layout.tsx
<CartProvider>{children}</CartProvider>

These are two different React trees. Crossing between them is a full page load and all in-memory state is destroyed. Fix: persist to cookies, localStorage, or the server — not React state.

4. Assuming the group name affects the URL

app/(dashboard)/settings/page.tsx

This is /settings, not /dashboard/settings. If you want the URL segment, drop the parentheses.

5. Treating a layout auth check as security

tsx
// app/(app)/layout.tsx
const session = await getSession()
if (!session) redirect('/login')

Good UX, insufficient security. Layouts don't re-run on every client navigation, and nothing stops a request hitting a Server Action or Route Handler directly. Fix: check authorization in the data access layer, close to the data. Chapter 18.

6. Over-grouping

app/
└── (site)/
    └── (public)/
        └── (content)/
            └── (blog)/
                └── posts/page.tsx

Four groups, one route, zero benefit. Add a group when you need a layout boundary or a genuine organizational win — not by reflex.

🎯 When & Why to Use

Route group with a layout    →  a section needs a different shell
Route group without a layout →  organizing files; no routing effect
Multiple root layouts        →  genuinely separate apps (different fonts,
                                CSS, <head>) crossed rarely
Regular folder               →  you actually want the URL segment
Private folder (_name)       →  colocated code that must never route

Decision flow:

Do these routes need a different wrapper UI?
├── No  → do they just belong together conceptually?
│         ├── Yes → route group, no layout
│         └── No  → leave them flat
└── Yes → Do they also need different <html>/<body>/fonts/global CSS?
          ├── No  → route group with a layout        ← most common
          └── Yes → multiple root layouts (accept the hard nav)

🏋️ Mini Practice Problems

Problem 1: URL mapping

List every URL this tree serves:

app/
├── layout.tsx
├── (public)/
│   ├── layout.tsx
│   ├── page.tsx
│   └── about/page.tsx
├── (dash)/
│   ├── layout.tsx
│   ├── home/page.tsx
│   └── (settings)/
│       ├── profile/page.tsx
│       └── security/page.tsx
└── _shared/
    └── Header.tsx

Problem 2: Fix the conflict

This build fails. Explain the error and give two different fixes:

app/
├── (marketing)/
│   └── page.tsx
└── (app)/
    └── page.tsx

Problem 3: Design it

An app needs:

  • Marketing pages with a serif font and a public nav
  • Auth pages (/login, /register) with a centered card, no nav at all
  • A dashboard with a sidebar and a sans-serif font, gated behind login
  • Marketing and dashboard share no CSS whatsoever

Sketch the app/ tree. Say where each layout.tsx lives, and whether app/layout.tsx exists.

Problem 4: Predict the cost

In your Problem 3 answer, what happens — technically — when a logged-in user clicks a link from /pricing to /dashboard? Is that acceptable? When would it not be?

💼 Interview Notes

Common Questions

Q: What is a route group and why would you use one? A folder wrapped in parentheses. It organizes routes and can introduce a layout boundary without contributing a URL segment. The main use is giving different sections of an app different shells at the same URL level.

Q: How do you give marketing pages and dashboard pages different layouts when both live at the root? Route groups: app/(marketing)/ and app/(app)/, each with its own layout.tsx. URLs stay at the root; layouts diverge.

Q: What are multiple root layouts and what do they cost? Delete app/layout.tsx and give each top-level group a layout containing <html> and <body>. Each group becomes an independent document with its own fonts, global CSS, and metadata. The cost is a full page reload when navigating between groups, and no shared client state.

Q: Can two route groups contain the same route? No. Parentheses are stripped when resolving URLs, so (a)/about and (b)/about both resolve to /about and the build fails with a parallel-pages error.

Q: Is putting an auth check in a group's layout enough to secure it? No. It's good UX — unauthenticated users get bounced early — but layouts don't re-run on every navigation, and Server Actions and Route Handlers don't pass through them. Authorization belongs next to the data.

🏢 Asked at Companies

  • Vercel: "Structure an app with a public site, an auth flow, and a gated dashboard. Where does each layout go?"
  • Linear: "When would you accept a full page reload between two sections of your app?"
  • Shopify: "A storefront and an admin panel share a codebase but no design system. How do you organize app/?"
  • Stripe: "A code review shows the only auth check is in a layout. What's your feedback?"

📊 Visual Memory Aid

              FOLDER NAME → URL EFFECT

  app/dashboard/page.tsx      →  /dashboard      (segment added)
  app/(dashboard)/page.tsx    →  /               (no segment)
  app/_dashboard/page.tsx     →  (not a route at all)


              ONE ROOT vs MANY ROOTS

  ┌── ONE ROOT LAYOUT ──────────────────────┐
  │  app/layout.tsx      <html><body>       │
  │    ├── (marketing)/layout.tsx           │
  │    └── (app)/layout.tsx                 │
  │                                          │
  │  /pricing → /dashboard = SOFT nav ⚡     │
  │  shared providers, shared state          │
  └──────────────────────────────────────────┘

  ┌── MULTIPLE ROOT LAYOUTS ────────────────┐
  │  (no app/layout.tsx)                     │
  │    ├── (marketing)/layout.tsx <html>     │
  │    └── (app)/layout.tsx       <html>     │
  │                                          │
  │  /pricing → /dashboard = HARD reload 🔄  │
  │  separate fonts, CSS, <head>, state      │
  └──────────────────────────────────────────┘


              LAYOUT BOUNDARY, NOT URL BOUNDARY

     app/
      ├─ (marketing)/    ┐
      │   layout.tsx     │  different shells
      ├─ (app)/          │  same URL depth
      │   layout.tsx     ┘

🎯 Key Takeaways

  1. (parentheses) create a layout boundary without a URL segment — the tool for giving sections different shells at the same URL level.
  2. Route groups are invisible to the router. Two groups can't own the same path, and the group name never appears in a URL.
  3. Multiple root layouts mean fully separate apps — own <html>, fonts, and CSS — at the cost of a hard reload and no shared state at the boundary.
  4. Grouping by shell gives you a natural place for section-wide concerns like an auth redirect, but a layout check is UX, not security.
  5. Don't group by reflex. Add a group when you need a layout boundary or a real organizational win, and leave the tree flat otherwise.

Next Chapter: Parallel & Intercepting Routes →

Practice: Restructure an existing app into (marketing), (auth), and (app) groups with three distinct layouts. Then try converting them to multiple root layouts, and observe the difference in the Network tab when navigating between groups.


PreviousChapter 5: Dynamic Routes & ParamsNextChapter 7: Parallel & Intercepting Routes

Open source, free forever. Built by iammhador.

Contribute on GitHub