In the App Router file-based system, the physical folder structure maps directly to public URLs. However, in enterprise-scale application architectures, codebase organizational requirements frequently conflict with public URL path requirements:
/login versus a persistent dashboard shell at /dashboard).The Next.js App Router solves this using Route Groups. Wrapping a directory name in parentheses instructs Turbopack that the segment exists purely for organizational structure and layout application, omitting it from the resolved URL path.
When the Next.js compiler scans the app directory, folder names enclosed in parentheses (folderName) act as filesystem dividers but are stripped from the URL address space during route table construction:
app/(marketing)/about/page.tsx compiles to /about.app/(dashboard)/analytics/page.tsx compiles to /analytics.app/(shop)/checkout/page.tsx compiles to /checkout.Concurrently, the compiler retains the group identity inside its internal Route Segment Tree ID. This enables each group to maintain its own independent layout.tsx, loading.tsx, and error.tsx.
Turbopack scans the app/ folder hierarchy and detects parenthesized directories like (marketing) and (dashboard).
Group names are omitted from the URL routing table: app/(marketing)/pricing/page.tsx registers as public /pricing.
The page binds to its group's layout: pricing/page.tsx wraps inside (marketing)/layout.tsx.
On navigation, Next.js checks ancestor layouts: if both pages share a Root Layout, a soft SPA transition executes; otherwise, a hard reload occurs.
The server emits a Flight stream containing only the target group's page and layout components without extraneous client bundles.
In a Next.js project, a file exists at app/(auth)/admin/login/page.tsx. What public URL will open this page in the browser?
Within a single application, different sections often require distinct layouts. Route Groups allow you to attach isolated layouts without affecting the public URL:
app/
├── (marketing)/
│ ├── layout.tsx # Public marketing layout (Header, Banner, Footer)
│ ├── page.tsx # Route: / (Marketing Home)
│ ├── about/page.tsx # Route: /about
│ └── pricing/page.tsx # Route: /pricing
└── (dashboard)/
├── layout.tsx # Private app shell (Sidebar, User menu)
├── dashboard/page.tsx # Route: /dashboard
└── settings/page.tsx # Route: /settingsNext.js permits deleting the root app/layout.tsx entirely and defining independent Root Layouts inside individual groups:
export default function MarketingRootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="marketing-theme">
{/* Clean static HTML without heavy client application providers */}
<header className="marketing-header">Marketing Shell</header>
{children}
</body>
</html>
);
}import { AuthProvider } from '@/components/auth/AuthProvider';
export default function AppRootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="app-theme">
{/* Heavy client context providers mount only here */}
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}What happens in the browser when a user navigates via a link from /pricing (under app/(marketing)/layout.tsx) to /dashboard (under app/(app)/layout.tsx)?
In commercial products, marketing landing pages must load with optimal Core Web Vitals (LCP < 1.2s). Using Route Groups isolates client session and websocket providers to the (app) group, leaving (marketing) entirely lightweight:
app/
├── layout.tsx (Shared Root Document Shell)
├── (marketing)/
│ ├── layout.tsx ──► Zero Client JS Bundle: Pure SSR/SSG
│ └── page.tsx ──► LCP: 0.6s (CDN Edge Cache)
└── (workspace)/
├── layout.tsx ──► Mounts AuthContext, WebSocketProvider, ReactQuery
└── editor/page.tsx ──► Interactive workspace applicationBecause parenthesized directory names are excluded from public URLs, defining identical relative paths in distinct groups triggers a fatal build error:
app/(marketing)/about/page.tsx $\to$ /aboutapp/(shop)/about/page.tsx $\to$ /about
Mitigation: Verify route uniqueness across groups and enforce directory linting rules (e.g., eslint-plugin-check-file).When an application splits into multiple Root Layouts, transitioning between them forces a full browser reload (window.location), wiping all unpersisted form state in browser memory.
Mitigation: Maintain a single shared app/layout.tsx for 95% of applications, utilizing Route Groups for nested layout variations.