Which Files Become URLs: page, route, and the Folders That Never Do
The App Router has one rule, and every routing question reduces to it: a folder becomes a URL only when it contains a route-producing special file — page or route. Everything else in app/ is either a modifier for a route that already exists, or ordinary code the router ignores. A fresh scaffold already demonstrates the rule:
app/
├── layout.tsx # wraps everything below it — no URL of its own
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── [slug]/
│ │ └── page.tsx # /blog/hello-world, /blog/next-15, …
│ └── archive/
│ └── page.tsx # /blog/archive
└── api/
└── health/
└── route.ts # GET /api/health
| Path | URL it serves |
|---|---|
app/page.tsx |
/ |
app/about/page.tsx |
/about |
app/blog/[slug]/page.tsx |
any /blog/<one-segment> |
app/docs/[...slug]/page.tsx |
/docs/a, /docs/a/b (required catch-all) |
app/shop/[[...slug]]/page.tsx |
/shop as well as /shop/a/b |
app/(marketing)/pricing/page.tsx |
/pricing — the group folder is invisible |
app/_scratch/notes/page.tsx |
nothing — a private folder is excluded entirely |
Two of those rows are where interviews go. Route groups (marketing) let you stack a different layout over a set of pages without adding a segment to the URL, so (marketing)/about and (dashboard)/about can both exist — which also means two groups accidentally declaring the same URL will collide at build time. Private folders _name are excluded from routing, which is how you keep experiment code inside app/ without publishing it.
The router matches names exactly: page.tsx, page.js, and page.jsx all register, but Page.tsx or pages.tsx do not — a case or plural slip produces a 404 with no warning in development on Windows or macOS, because those filesystems ignore case and Linux does not. The same exactness applies to the special files themselves; there is no partial credit in file-convention routing.
The counterpart is route.ts, which is an endpoint rather than a page:
// app/api/health/route.ts
export async function GET() {
return Response.json({ ok: true, uptime: process.uptime() })
}
Export HTTP method functions (GET, POST, PATCH, DELETE) and the folder becomes an API route — the App Router's replacement for pages/api/*.ts. Two conventions to respect: it must be .ts/.js, not .tsx, because there is no JSX in it; and a segment serves one purpose, so choose page.tsx for UI or route.ts for an endpoint in a given folder rather than trying to host both.
Notice what does not produce a URL. A folder with no page or route file — app/components/, app/dashboard/widgets/ — renders nothing when visited. That is the biggest behavioral shift from the Pages Router, where every .tsx file in pages/ became a route and shipping pages/components/Button.tsx publicly exposed /components/Button as a live URL. In app/, you may colocate ProductCard.tsx, schema.ts, and helpers.test.ts next to the page that uses them; the router scans for special file names only, so the code stays route-private by default.
The failure mode to hold onto: a folder named for something internal (app/utils/, app/(old)/legacy/) silently stays harmless — until someone drops a page.tsx inside, and the URL appears in production with no configuration change, no route table entry to review, no warning. File location is the public API of your site, which is precisely why Scaffolding with create-next-app puts such weight on choosing app/ (or src/app/) deliberately: that directory is where every URL you will ever ship is declared.
layout, loading, error: Which Special File Runs Where
If page and route decide what is served, the remaining special files decide how a segment behaves while it is served. Each one has exactly one job, and each has a granularity that interviewers like to test.
| File | Job | Scope |
|---|---|---|
page.tsx |
UI for this segment's URL | that URL only |
route.ts |
HTTP endpoint (GET, POST, …) |
that URL only |
layout.tsx |
Wrapper around children; persists across navigation | segment and everything nested under it |
template.tsx |
Like a layout, but remounts on every navigation | segment and below |
loading.tsx |
Instant fallback while the segment's data resolves | one Suspense boundary |
error.tsx |
Recoverable UI when rendering below it throws | one error boundary |
not-found.tsx |
404 UI for notFound() or an unmatched URL at this segment |
segment and below |
default.tsx |
Fallback when a parallel route (@slot) has no match |
that slot |
Three details cause most bugs.
error.tsx must be a Client Component. It is an error boundary, and React error boundaries only exist in the browser tree, so the file starts with 'use client' and receives an error object plus a reset() function:
'use client'
export default function Error({
error,
reset,
}: { error: Error & { digest?: string }; reset: () => void }) {
return (
<div>
<h2>Could not load this order</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
The second subtlety: an error.tsx catches failures thrown below it — typically the page.tsx in its own folder — but it does not catch errors thrown by the layout.tsx sitting beside it. A crash in the same segment's layout bubbles up to the parent's boundary, which is why a broken root layout takes down every route in the app instead of showing your nicest fallback.
loading.tsx is a Suspense boundary, not a spinner service. It renders the moment a navigation starts, inside the parent's layout, while the child page streams:
/navigation → /dashboard/orders
┌──────────────────────────────────┐ ← root layout: never re-renders
│ App bar │
│ ┌──────────────────────────────┐ │ ← loading.tsx for /dashboard/orders
│ │ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓ │ │ appears instantly
│ └──────────────────────────────┘ │
└──────────────────────────────────┘
Because it lives under the parent layout, the shell stays visible and the user keeps their place — the mechanism behind "instant navigation" in every App Router demo. Drop it in and the whole view blanks until the slow await fetch resolves.
Layouts compose and persist. app/layout.tsx wraps app/dashboard/layout.tsx, which wraps the matched page; each receives its nested UI through a children prop. On client-side navigation, layouts above the matched segment are not re-rendered, so open sidebars, scroll position, and useState survive a route change — a page component, by contrast, unmounts and remounts every time. template.tsx exists for the rare case where you want the opposite: a wrapper that re-creates its subtree on each navigation, for example a component that replays an entrance animation.
Recognizing these names as behavior boundaries rather than routes is the interview payoff: the question "what does loading.tsx actually do?" is answered by naming Suspense, the parent layout, and streaming — not by saying "it shows a spinner". The next file, Environment Variables, moves from file conventions to configuration, but the segment model here is what every later topic builds on.
Quiz
1. A folder under app/ contains only `layout.tsx` and `loading.tsx`, with no page or route file. What happens when you visit its path?
2. Which of these App Router files must begin with 'use client'?
3. You need a Button used by four routes, and it must never be routable. Where does it go?
4. What does adding `app/(marketing)/pricing/page.tsx` do to the site's URLs?
Flashcards
Question
Which two file names produce routable URLs under app/?
Click to reveal answer
Answer
page (renders the UI for a segment) and route (exports HTTP methods for an endpoint). Every other special file only adjusts behavior for routes that already exist, and folders containing neither never respond to a request.
Question
What is the difference between a route group `(name)` and a private folder `_name`?
Click to reveal answer
Answer
(group) folders organize code and scope layouts but are invisible in the URL, so two groups can declare the same path and collide. _private folders are excluded from routing entirely, so nothing inside them is ever matched, even indirectly.
Question
Why must error.tsx be a client component, and what does it fail to catch?
Click to reveal answer
Answer
React error boundaries exist only in the client tree, so the file needs 'use client' and receives `error` plus `reset()`. It catches failures thrown below it (the page) but not errors thrown by the layout.tsx beside it in the same segment — those bubble to the parent's boundary.
Question
How does layout.tsx behave across client-side navigation compared with template.tsx?
Click to reveal answer
Answer
Layouts are preserved when navigating below them, so component state such as an open sidebar or scroll position survives, and they receive nested output via `children`. Templates remount on every navigation, re-creating the subtree each time — useful for replaying entrance animations.
Question
Where does shared code live, and what promotes a file out of a route folder?
Click to reveal answer
Answer
In src/components, src/lib, src/hooks, src/types — outside app/ they can never match a route. Colocate route-private files with their segment, and hoist to a shared folder the moment a second route imports them.
Revision Notes
Key Takeaways
- 1. Routing has exactly two producers — page.tsx and route.ts — and folders without them do not respond, which is why colocation inside app/ is safe by default
- 2. Route groups `(x)` vanish from URLs while private folders `_x` vanish from routing; both are how you organize without publishing
- 3. loading.tsx is a Suspense boundary rendered inside the parent layout, and error.tsx is a client-side boundary that does not protect its own segment's layout
- 4. Layouts persist across navigation (state survives) while page components unmount and remount — the mechanism interviewers probe with state-preservation questions
- 5. Never give an ordinary component a special file name, and treat every new page.tsx as publishing a public URL with no review step
Interview Tips
- • "How does the App Router decide what is routable?" — folders become segments, page/route register the URL, special files modify behavior, and everything else in app/ is ignored; state it as a rule and then show you know the exceptions (groups, private folders)
- • "Where do you put shared components?" — outside app/ in src/components, imported via the @ alias, with the promotion rule: colocate until a second route imports it, then hoist
- • "What does loading.tsx actually do?" — it becomes a Suspense boundary for the segment, streams instantly under the parent layout during navigation, and keeps the shell visible while data resolves
- • "Why can't a layout be a client component?" — it can, but everything below it then ships JavaScript; the default is a server layout with a 'use client' island for the interactive parts, which keeps the payload small
Cheat Sheet
App Directory Conventions
What creates a URL (the only two):
| File | Result |
|---|---|
page.tsx |
UI route for that segment (app/about/page.tsx → /about) |
route.ts |
HTTP endpoint exporting GET/POST/… (app/api/health/route.ts) |
What does not: any other folder in app/, files without special names, and everything outside app/ (src/components, src/lib, src/hooks). route.ts must be .ts/.js, never .tsx.
URL-shaping folders:
| Folder | Effect on URL |
|---|---|
(group) route group |
layout scoping only — segment is invisible |
_private private folder |
excluded from routing entirely |
[slug] / [...slug] / [[...slug]] |
dynamic / catch-all / optional catch-all |
Behavior modifiers (no URL of their own):
layout.tsx— wraps children, persists across navigation (state survives)template.tsx— like a layout but remounts every navigationloading.tsx— Suspense boundary, renders inside the parent layout while the segment streamserror.tsx— error boundary; must start with'use client'; does not catch its own segment's layoutnot-found.tsx—notFound()and unmatched URLsdefault.tsx— fallback for a parallel route@slotwith no match
Shared code placement:
- Colocate route-private files (
order-card.tsx,status.ts) in the segment folder. - Promote to
src/components/src/libwhen a second route imports them. - Never name a plain component
loading.tsx/error.tsx— the router registers it.
Interview one-liners:
- "How does routing decide?" → folder +
page/route; no route config file exists. - "Why doesn't
app/components/create a route?" → only special files register, and it has none. - "What happens if I add
page.tsxto an internal folder?" → the URL goes live immediately, with no other change.