Route groups and private folders
Use (group) folders to sort routes and give each section its own layout without changing URLs, and _private folders to keep your files out of routing.
- 01Group routes without changing URLsSort pages into (group) folders and say which address each page still has.
- 02Give each section its own layoutAdd a layout to one group, or a whole root layout, and know when that costs a full page load.
- 03Keep your own files out of routingUse _private folders and pick one way to place components and data, and use it everywhere.
Round brackets keep a folder out of the address
In File-system routing, each ordinary folder on the way to a page added one segment to the address: app/teas/page.tsx was the page for /teas. That works until you want to sort your folders in a way the address shouldn't show. The tea shop has pages for shopping, like the teas and the cart, and pages about the shop, like About. You want them in two folders, but you don't want addresses like /shop/teas.
A folder whose name is in round brackets, like (shop), is a route group. It sorts the files inside it, but it isn't part of the address. Here is the shop with two groups. Like every example in this lesson, it's a real Next.js 16.4.3 project, built with next build and run with next start, and what it sent back was recorded. Click through its files, then its pages.
app/(shop)/teas/page.tsxexport default function TeasPage() { return ( <main> <h1>Our teas</h1> <ul> <li>Green tea: $4</li> <li>Mint tea: $3</li> </ul> </main> );}- Green tea: $4
- Mint tea: $3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
The file shown first, app/(shop)/teas/page.tsx, is the same teas page as before: line 1 exports it, and line 4 is its heading, Our teas. Only its place changed. It now sits in (shop), next to cart, and about sits in (info). app/page.tsx and the root layout, app/layout.tsx, stay at the top, outside both groups.
Open the Terminal tab. The route table that next build prints lists ○ /, ○ /_not-found, ○ /about, ○ /cart and ○ /teas. The ○ means "prerendered as static content": each page was made once, during the build. There's no (shop) and no (info) in any address. In the Browser tab, pick /shop/teas and /(shop)/teas: both answered 404, the page for addresses that don't exist. The group's name isn't in the address, with round brackets or without.
Each row is one page file of this shop: the group it's in, and the address the build gave it.
| File | Its group | Its address |
|---|---|---|
app/page.tsx | None | / |
app/(shop)/teas/page.tsx | (shop) | /teas |
app/(shop)/cart/page.tsx | (shop) | /cart |
app/(info)/about/page.tsx | (info) | /about |
(shop) and (info) sort the files, but each page keeps the address of its other folders: /teas, /cart and /about.A library sorts its books into corners, each with a sign. The sign helps you find your way, but a book's number stays the same whichever corner it sits in, and you ask for a book by its number. A corner can have its own carpet and lamps, which only the books there get. Behind a door, the staff room holds things the library uses, and no visitor can ask for them.
- In real life: A corner of the library with a sign, like "Children's books"
- In Next.js: A route group, like
(shop) - In real life: The number on a book's spine, which you use to ask for it
- In Next.js: A page's address, like
/teas - In real life: The corner's own carpet and lamps
- In Next.js: A
layout.tsxinside the group, around its pages only - In real life: A staff room, where visitors can't ask for anything
- In Next.js: A private folder, like
_components, which has no address
A folder in round brackets, like (shop), is a route group: it sorts routes without changing their addresses. A folder that starts with an underscore, like _components, is a private folder: nothing inside it becomes a route. The rest of this lesson shows what each one is for.
A layout for each section
Sorting files is only half of what groups do. The bigger reason is layouts. A layout.tsx in a folder goes around every page in that folder and in the folders below it (the Layouts and pages lesson shows layouts in full). Without groups, a layout for only the teas and the cart would need a folder they share, and that folder would add a segment: /shop/teas. A group gives them a folder to share with no segment.
Here, each group gets a layout. The shop's pages get a line about delivery, and the info pages get the opening hours at the bottom. A checkout page is added outside both groups:
app/(shop)/layout.tsxexport default function ShopLayout({ children }: LayoutProps<"/">) { return ( <> <p>Free delivery on orders over $20.</p> {children} </> );}Free delivery on orders over $20.
- Green tea: $4
- Mint tea: $3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/(shop)/layout.tsx, line 4 is the delivery line, and line 5, {children}, is where the page goes. In app/(info)/layout.tsx, line 4 places the page first and line 5 puts the opening hours under it. Line 1 of each types the props with LayoutProps<"/">, a type Next.js writes for you. The route it names is "/", not "/(shop)", because the group adds nothing to the address. Exercise 4 shows what the type check says otherwise.
Now pick each page in the Browser tab. /teas and /cart show Free delivery on orders over $20. above the page. /about shows Open every day, 9 to 5. under it. /checkout and / show only the root layout's links. The Terminal tab still lists plain addresses, with ○ /checkout added. Each row is one recorded page, with what goes around it, from the outside in:
| Address | Layouts around the page | What the visitor sees besides the page |
|---|---|---|
/teas | The root layout, then (shop)/layout.tsx | The links, then Free delivery on orders over $20. |
/cart | The root layout, then (shop)/layout.tsx | The links, then Free delivery on orders over $20. |
/about | The root layout, then (info)/layout.tsx | The links, and Open every day, 9 to 5. under the page. |
/checkout | Only the root layout | Only the links. |
/ | Only the root layout | Only the links. |
/teas gets the root layout and (shop)'s, /about gets (info)'s, and /checkout, outside both groups, gets only the root layout.This is also how you keep one page out of a layout that its neighbors share. The checkout page shouldn't tempt anyone to keep shopping, so it sits outside (shop). Try both places. Before you click, guess what /checkout shows in each:
app/checkout/page.tsxexport default function CheckoutPage() { return ( <main> <h1>Checkout</h1> <p>Enter your address to finish your order.</p> </main> );}Enter your address to finish your order.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Checkout sits outside both groups, so only the root layout goes around it: /checkout shows the links, then Checkout, with no delivery line.
Inside (shop), the checkout page gets the delivery line. Outside it, it doesn't. Its address is /checkout both times. Moving a folder in or out of a group changes which layouts go around its pages, and never their addresses.
One address, one page
Groups don't add segments, so two pages in two groups can end up with the same address. Say the shop wants a page about its teas in (shop), and puts it in about/page.tsx there too. Both files would be /about. Pick the second version to see what next build does with that:
app/(info)/about/page.tsxexport default function AboutPage() { return ( <main> <h1>About us</h1> <p>Asha and Ravi run the shop.</p> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (7/7) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ○ /about├ ○ /cart├ ○ /checkout└ ○ /teas ○ (Static) prerendered as static contentOne About page, in (info). The build lists ○ /about, and it means one page.
The build stops with You cannot have two parallel pages that resolve to the same path. "Resolve to the same path" means both page files would answer the same address. Next.js doesn't pick one for you. Each address must belong to exactly one page, so give the second page an address of its own, like app/(shop)/teas/about/page.tsx for /teas/about.
Two root layouts in one app
The root layout is the layout at the top of the app. It's the one that writes <html> and <body>, and every page sits inside it. So far, that was app/layout.tsx. To be exact, a root layout is any layout with no other layout above it. Delete app/layout.tsx and give each group a layout with its own <html> and <body>, and the app has two root layouts.
That's useful when parts of one app look completely different. Here, the shop gets a blog called Tea notes, with its own header and its own page title:
app/(blog)/layout.tsximport Link from "next/link"; export const metadata = { title: "Tea notes" }; export default function BlogRootLayout({ children }: LayoutProps<"/">) { return ( <html lang="en"> <body> <header> <p>Tea notes, a blog from the tea shop</p> <Link href="/">Back to the shop</Link> </header> {children} </body> </html> );}Fresh tea, sent to your door.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
The file shown first is app/(blog)/layout.tsx. Line 3 gives the blog's pages the title Tea notes, lines 7 and 8 open its own <html> and <body>, and line 11 links back to the shop. In app/(shop)/layout.tsx, the same lines make the shop's document, with the title Tea shop on line 3 and the links Home, Teas and Blog on line 10. There's no app/layout.tsx. The home page moved into the group too, as app/(shop)/page.tsx, so that a root layout goes around it. In the Browser tab, / and /teas have the shop's links, and /blog has the blog's header.
Moving between the two has a cost. Inside one root layout, a click on a Link is a client-side navigation: the page's JavaScript swaps in the new page, and the layout and everything else on the screen stay (How Next.js works showed this). Between two root layouts that's impossible: the old <html> can't stay, because the new page needs its own. So Next.js does a full page load instead: the browser asks for a whole new HTML document and starts over, as if you typed the address.
This example was also clicked through in real Chrome. The Steps tab below lists four steps: open /, click Teas, click Blog, and click Back to the shop. Each step shows the page after it and the requests the page made, without the script and style files. It opens on step 3.
app/(blog)/layout.tsximport Link from "next/link"; export const metadata = { title: "Tea notes" }; export default function BlogRootLayout({ children }: LayoutProps<"/">) { return ( <html lang="en"> <body> <header> <p>Tea notes, a blog from the tea shop</p> <Link href="/">Back to the shop</Link> </header> {children} </body> </html> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
Tea notes, a blog from the tea shop
Back to the shopUse water just below boiling, and wait three minutes.
- Focus
- nothing: the page itself
- Scrolled
- 0 px down
Requests the page made during this step (3), without the files it loaded
GET/blog200a whole pageGET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /_tree
- next-url
- /blog
GET/200text/x-component- rsc
- 1
- next-router-prefetch
- 1
- next-router-segment-prefetch
- /!KHNob3Ap/__PAGE__
- next-url
- /blog
- Step 1, open
/: one request for a whole page,GET /, and then requests markedtext/x-component. Those are prefetches: the page's JavaScript asking early for the linked pages,/teasand/blog, before anyone clicks. Both are○pages, made during the build, so the prefetches bring all of them. - Step 2, click Teas: no requests at all.
/teasshares the shop's root layout, and its prefetch had already arrived, so the page was swapped in place. - Step 3, click Blog:
GET /blog, marked a whole page./blogwas prefetched in step 1 too, but it has another root layout, so Next.js loaded it as a new document. - Step 4, click Back to the shop: the same again the other way:
GET /, a whole page.
<html>. Inside one, Next.js swaps only the page. Going to the other one loads a whole new page.A full page load means the browser runs the page's JavaScript again from the start, and anything only kept in the browser, like a half-typed form or a Client Component's state, is gone. Within the shop, or within the blog, nothing changes. So split an app into root layouts only where visitors rarely cross, like a shop and its blog, or a public site and a signed-in app. Next.js makes this decision by comparing the two routes' root layouts, in is-navigating-to-new-root-layout.ts if you want to read it.
Without app/layout.tsx, a page outside both groups has no root layout above it, and next build stops. That's why the home page moved to app/(shop)/page.tsx. Exercise 5 shows the error.
Private folders: _folder
Project structure gave a first look at the other special folder name: a folder whose name starts with an underscore, like _components, is a private folder. Next.js leaves it out of routing, with every folder and file inside it. Here the teas page draws each tea with a card from its own private folder, and someone left a preview page in there too:
app/(shop)/teas/page.tsximport TeaCard from "./_components/tea-card"; export default function TeasPage() { return ( <main> <h1>Our teas</h1> <ul> <TeaCard name="Green tea" price={4} /> <TeaCard name="Mint tea" price={3} /> </ul> </main> );}Free delivery on orders over $20.
- Green tea: $4
- Mint tea: $3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Line 1 of app/(shop)/teas/page.tsx imports TeaCard from ./_components/tea-card, and lines 8 and 9 draw a card for each tea. The /teas page shows Green tea: $4 and Mint tea: $3, so the import works like any other.
Now pick /teas/_components/preview. There's a page.tsx at app/(shop)/teas/_components/preview/page.tsx, two folders down, and still the server answered 404. So did /teas/_components/tea-card. The route table in the Terminal tab has no address with _components in it. An underscore at the start of a folder's name switches off routing for everything inside it, page files and route.ts files too.
The last address, /_help, answered 200 with the help page. What if you really want an address that starts with an underscore? Write the underscore as %5F, the way URLs write it in code: the folder app/%5Fhelp is the address /_help, and the build lists ○ /_help.
_ takes itself and everything inside it out of routing, even a page.tsx. To get an address that really starts with an underscore, name the folder %5F….Now the same folder without its underscore. Guess first: does the preview page get an address?
app/(shop)/teas/_components/preview/page.tsxexport default function CardPreview() { return ( <main> <h1>Tea card preview</h1> </main> );}The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
The underscore makes _components a private folder. The preview's page.tsx has no address: the route table has no new route, and /teas/_components/preview answers 404.
Without the underscore, components is an ordinary folder, and its page.tsx made a page anyone can open, at /teas/components/preview. That's the mistake a private folder rules out.
You don't need a private folder to keep a component beside a page: a tea-card.tsx straight in teas never gets an address either, as File-system routing showed. Private folders are still worth it:
- They keep the folders that make routes apart from the folders that hold your code, so a glance tells them apart.
- A stray
page.tsxorroute.tsinside one never becomes a route, as the preview page showed. - Their names all start with
_, so editors list them together. - No file in one can clash with a special file name, like
loading.tsx, including names Next.js adds in later versions.
A private folder only keeps files from becoming routes. Code in it still reaches the browser if a Client Component imports it, and whatever a page shows is sent to the browser. Keep secrets, like API keys, in code that only runs on the server. The Server-only code and environment variables lesson shows how.
Three ways to place your own files
Keeping a file next to the code that uses it is called colocation. Next.js doesn't mind where your components and data live, so teams pick a pattern. Each row is one of the three common ones:
| Pattern | Where the files go | What it gives you |
|---|---|---|
Outside app | In folders beside app, like components and lib. | app holds only routes. Everything else is one folder away. |
In private folders at the top of app | In app/_components and app/_lib. | All of the code is inside app, and none of it can become a route. |
| Beside the route that uses it | Shared code at the top; a page's own parts in its folder, like teas/_components. | A page and its parts sit together, so you find them together. |
The patterns mix well. Here is the tea shop using all three: the logo in components beside app, the teas in a private folder at the top of app, and the tea card in a private folder beside the teas page.
app/
(info)/
about/
page.tsx
layout.tsx
(shop)/
cart/
page.tsx
teas/
_components/
tea-card.tsx
page.tsx
layout.tsx
_lib/
teas.ts
checkout/
page.tsx
layout.tsx
page.tsx
components/
logo.tsx(info) and (shop) sort the pages and hold their layouts. _lib/teas.ts is shared data that any page can import, and teas/_components/tea-card.tsx belongs to the teas page alone. components/logo.tsx sits outside app. The next section runs this shop. Whichever pattern you choose, use it across the whole project, so everyone knows where to look.
The whole tea shop, organized
Here is the organized shop, built and run. The file shown first is the teas page:
app/(shop)/teas/page.tsximport { teas } from "@/app/_lib/teas";import TeaCard from "./_components/tea-card"; export default function TeasPage() { return ( <main> <h1>Our teas</h1> <ul> {teas.map((tea) => ( <TeaCard key={tea.id} name={tea.name} price={tea.price} /> ))} </ul> </main> );}Free delivery on orders over $20.
- Green tea: $4
- Mint tea: $3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Line 1 imports the teas from @/app/_lib/teas. @/ means the project's top folder, set in tsconfig.json. Line 2 imports the card from the private folder beside the page, and line 10 draws one for each tea. In app/layout.tsx, line 2 imports the logo from components.
Open the Terminal tab: the route table is /, /_not-found, /about, /cart, /checkout and /teas, the same addresses as before the shop was organized. In the Browser tab, /teas and /about work, each with its group's layout, and /_lib/teas and /components/logo answered 404. Organizing the files changed nothing a visitor can see.
Now sort the folder names from this lesson: which ones become part of an address?
teas(shop)_components%5Fhelpcheckout(blog)
Sort each folder name: does it become a part of the address, or is it never in the address?
Common mix-ups
- "A route group adds its name to the address." No.
/shop/teasand/(shop)/teasanswered404; the page is/teas. - "Two groups can each have their own
/about." No. Both would be/about, and the build stops. - "A group's layout goes around every page." No. Only around the pages inside the group:
/checkoutdidn't get the shop's layout. - "Moving between root layouts is a normal client-side navigation." No. Clicking Blog loaded
/blogas a whole new page. - "A
page.tsxinside a private folder still makes a page." No./teas/_components/previewanswered404. - "A file in a private folder is hidden from the browser." No. It only has no address. A Client Component that imports it sends its code to the browser.
Each row is one kind of folder name: whether it becomes part of the address, and whether its pages can be visited.
| Folder name | Is it part of the address? | Can its pages be visited? |
|---|---|---|
teas | Yes: it's /teas. | Yes, if it has a page.tsx. |
(shop) | No. Its pages keep their addresses. | Yes. Each page in it keeps its own address. |
_components | No. | No. Nothing inside it is a route, even a page.tsx. |
%5Fhelp | Yes, as /_help. | Yes, if it has a page.tsx. |
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
Which address does app/(shop)/cart/page.tsx make?
app/
(info)/
about/
page.tsx
(shop)/
cart/
page.tsx
teas/
page.tsx
layout.tsx
page.tsx/cart. (shop) is a route group, so only cart is a segment. The recorded build lists ○ /cart, and /cart showed Your cart.
What does /teas/_components/preview answer?
app/
(shop)/
teas/
_components/
preview/
page.tsx
tea-card.tsx
page.tsx404. _components is a private folder, so the page.tsx two folders inside it has no address. The recorded request answered 404.
The shop wants the delivery line on its checkout page after all. Where does checkout/page.tsx go?
app/
(info)/
about/
page.tsx
layout.tsx
(shop)/
cart/
page.tsx
teas/
page.tsx
layout.tsx
checkout/
page.tsx
layout.tsx
page.tsxapp/(shop)/checkout/page.tsx. Inside (shop), the shop's layout goes around it. The recorded /checkout then shows Free delivery on orders over $20. above Checkout, at the same address.
With this app/(shop)/layout.tsx, the build fails its type check: Type '"/(shop)"' does not satisfy the constraint '"/"'. What goes between the angle brackets?
export default function ShopLayout({ children }: LayoutProps<"/(shop)">) {
return (
<>
<p>Free delivery on orders over $20.</p>
{children}
</>
);
}
export default function ShopLayout({ children }: LayoutProps<"/">) {
return (
<>
<p>Free delivery on orders over $20.</p>
{children}
</>
);
}
LayoutProps<"/">. A group adds no segment, so the layout in (shop) sits at /. The recorded build failed with Type '"/(shop)"' does not satisfy the constraint '"/"', and passed with "/".
This shop has two root layouts. The build stops with about/page.tsx doesn't have a root layout. To fix this error, make sure every page has a root layout. Where should the About page go, so that it shows with the shop's links and keeps the address /about?
app/
(blog)/
blog/
page.tsx
layout.tsx
(shop)/
teas/
page.tsx
layout.tsx
page.tsx
about/
page.tsxexport default function AboutPage() {
return (
<main>
<h1>About us</h1>
<p>Asha and Ravi run the shop.</p>
</main>
);
}
app/(shop)/about/page.tsx. Without app/layout.tsx, only pages inside a group have a root layout. Moved into (shop), the recorded build passes, and /about shows the shop's links and About us.
The shop wants a help page at /_help, but app/_help/page.tsx answers 404. What should the folder be called?
app/
_help/
page.tsx
layout.tsx
page.tsxexport default function HelpPage() {
return (
<main>
<h1>Help</h1>
<p>Write to us if a box arrives late.</p>
</main>
);
}
%5Fhelp. %5F is an underscore written for URLs, so app/%5Fhelp/page.tsx is /_help. The recorded build lists ○ /_help, and it answered 200 with Help.
Check your understanding
Question 1 of 7Which address does
app/(shop)/cart/page.tsxmake?Choose an answer to see the explanation.
Question 2 of 7In this shop, which pages show the layout in
(shop)/layout.tsx?Read the code, then predictTextapp/ (info)/ about/ page.tsx layout.tsx (shop)/ cart/ page.tsx teas/ page.tsx layout.tsx checkout/ page.tsx layout.tsx page.tsxChoose an answer to see the explanation.
Question 3 of 7Both
(shop)and(info)get anabout/page.tsx. What happens?Choose an answer to see the explanation.
Question 4 of 7In the shop with two root layouts, a visitor on
/teasclicks the Blog link. What does the browser do?Choose an answer to see the explanation.
Question 5 of 7
app/(shop)/teas/_components/preview/page.tsxexists. What does/teas/_components/previewanswer?Choose an answer to see the explanation.
Question 6 of 7Which folder name makes the address
/_help?Choose an answer to see the explanation.
Question 7 of 7Which type fits the layout in
app/(shop)/layout.tsx, whenapp/layout.tsxis the only other layout?Choose an answer to see the explanation.
Key takeaways
- A folder in round brackets, like
(shop), is a route group. It sorts routes and never shows in the address:app/(shop)/teas/page.tsxis/teas. - A
layout.tsxin a group goes around the pages in that group only. Move a folder in or out to choose its layouts. - Two groups can't both make the same address: the build stops.
- Without
app/layout.tsx, each group can have its own root layout. Every page needs one, and moving between two root layouts is a full page load. - A folder starting with
_is private: nothing inside it is a route.%5Fgives a real underscore in the address. - No address doesn't mean secret: keep secrets in code that only runs on the server.
Remember the one-liner.
Round brackets sort without changing the address, and an underscore keeps a folder out of routing.
Coming next: Loading UI. It shows loading.tsx, the file that shows a loading state while a page waits for its data.