
In the previous article, we saw how Server Components and the RSC Payload revolutionize rendering.
But there's another innovation in Next.js that's just as fundamental: file-system routing.
Instead of configuring routes in a separate file (like React Router's <Routes> or Vue's router.js), Next.js derives your application's URL structure directly from your folder hierarchy. This isn't just a convenience — it fundamentally changes how you organize code.
Folders define URL segments.
Files define the UI for those segments.
A route becomes public when a page.tsx or route.ts file exists.Here's what that looks like in practice:
app/
├── layout.tsx ← Wraps all routes (root layout)
├── page.tsx → /
├── blog/
│ ├── layout.tsx ← Wraps /blog and its children
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/hello-world
└── about/
└── page.tsx → /aboutEvery folder inside app/ maps to a URL segment. Every page.tsx makes that segment publicly accessible. The mapping is one-to-one: no configuration, no route table, no central registry.
When a page renders, six types of special files compose the UI in a specific order:
layout.tsx ← Outermost wrapper. Persists across navigations.
template.tsx ← Like layout, but re-mounts on every navigation.
error.tsx ← React Error Boundary. Catches rendering errors.
loading.tsx ← React Suspense Boundary. Shows fallback while loading.
not-found.tsx ← Catches 404s within this segment.
page.tsx ← The actual page content.This hierarchy is recursive — each nested route gets wrapped by its parent's layout and error/loading boundaries.
The critical difference between layout and template:
// layout.tsx — state is preserved during navigation
export default function BlogLayout({ children }) {
const [search, setSearch] = useState('') // Survives navigation between /blog/xxx and /blog/yyy
return <div>{children}</div>
}
// template.tsx — re-mounts on every navigation
export default function BlogTemplate({ children }) {
const [viewCount, setViewCount] = useState(0) // Resets to 0 on every navigation
return <div>{children}</div>
}Use layout for shared UI that should feel persistent (navigation bars, sidebars). Use template when you need to reset state or trigger animations on every visit.
Square brackets create parameterized segments:
app/blog/[slug]/page.tsx → /blog/hello-world (single param)
app/shop/[...slug]/page.tsx → /shop/clothing/shirts (catch-all)
app/docs/[[...slug]]/page.tsx → /docs (optional catch-all)The difference between [...slug] and [[...slug]]:
// [...slug] — REQUIRES at least one segment
// /shop → 404 (not matched)
// /shop/clothing → slug = ['clothing']
// [[...slug]] — the route itself is matched
// /docs → slug = undefined
// /docs/api/auth → slug = ['api', 'auth']Dynamic segments pair with generateStaticParams to enable static generation:
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // params is a Promise in Next.js 15+
const post = await getPost(slug)
return <h1>{post.title}</h1>
}During next build, Next.js calls generateStaticParams, gets the list of all slugs, and pre-renders every [slug] page at build time. Pages for slugs not in the list are rendered dynamically at request time (or return 404 if configured).
Parentheses create organizational folders that don't appear in the URL:
app/
├── (marketing)/
│ ├── layout.tsx ← Marketing-specific layout
│ ├── page.tsx → /
│ └── pricing/
│ └── page.tsx → /pricing
└── (dashboard)/
├── layout.tsx ← Dashboard-specific layout (with auth check)
└── analytics/
└── page.tsx → /analyticsRoute groups solve three problems:
<html> and <body> tags, enabling completely different UI shells for different sections.Folders prefixed with _ are excluded from routing entirely:
app/
├── blog/
│ ├── _components/ ← Not routable. Safe for internal UI code.
│ │ └── PostCard.tsx
│ ├── _lib/ ← Not routable. Safe for utilities.
│ │ └── formatDate.ts
│ └── page.tsx → /blogThis is useful for colocating components, utilities, and types next to the routes that use them, without accidentally exposing URLs.
Important: files in
app/are already safe from routing (onlypage.tsxandroute.tscreate public routes). But_prefixes make the intent explicit and prevent name conflicts with future Next.js file conventions.
layout → template → error → loading → page wraps at every nesting level.layout persists state; template re-mounts. Choose based on whether you want state to survive navigation.[...slug] requires segments; [[...slug]] makes them optional. This controls whether the bare route (without parameters) matches.(name) organize code without changing URLs, enabling multiple root layouts._name exclude directories from routing, making colocation safe.Previous:
Next:
The module graph boundary, interleaving patterns, and context provider bridging.