File-based routing and layouts
How folders in app/ become URLs, how nested layouts persist across navigation, and the special files that handle loading and errors.
Folders are routes
In the App Router, a directory under app/ becomes a URL segment, and a page.js inside it makes that segment addressable. Files without an export named default page do not create routes.
| Path on disk | URL | Notes |
|---|---|---|
app/page.js | / | Home page |
app/blog/page.js | /blog | Static segment |
app/blog/[slug]/page.js | /blog/intro | Dynamic segment, one level |
app/docs/[...path]/page.js | /docs/a/b | Catch-all segments |
app/shop/[[...filters]]/page.js | /shop and /shop/x | Optional catch-all |
app/(marketing)/about/page.js | /about | Route group: folder is skipped in the URL |
// app/blog/[slug]/page.js
export default async function PostPage({ params }) {
const { slug } = await params // Next.js 15: params is a Promise
const post = await getPost(slug)
if (!post) notFound()
return <article>{post.title}</article>
}
// build-time prerender for known slugs
export async function generateStaticParams() {
const posts = await listPosts()
return posts.map(p => ({ slug: p.slug }))
}page.jsis the route,layout.jswraps the segment and its children, androute.jsreplaces the page with an HTTP handler for that path.- Dynamic segment names must match the folder:
[slug]givesparams.slug. - From Next.js 15,
paramsandsearchParamsare promises —awaitthem, or read them with React'suse()in a client component. notFound()renders the nearestnot-found.jsand returns the right 404 status.
Nested layouts that survive navigation
// app/layout.js — required root layout
import './globals.css'
export const metadata = { title: { default: 'Guides', template: '%s | Guides' } }
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<SiteHeader />
<main>{children}</main>
</body>
</html>
)
}// app/docs/layout.js — wraps every route under /docs
export default function DocsLayout({ children }) {
return (
<div className="docs">
<DocsSidebar />
{children}
</div>
)
}- Layouts nest: the root layout wraps everything, and each segment can add its own shell. Only the parts that change re-render on navigation, and layout state is preserved.
- A layout receives
childrenbut never the current route'sparamsfrom a deeper level — keep per-route work in the page. - Use
template.jsinstead oflayout.jswhen you need a fresh instance per navigation (an entrance animation, or a form that resets). - Route groups with parentheses let you share a layout across unrelated URLs without putting that segment in the path.
Loading, error and not-found files
// app/blog/loading.js — shown instantly while the page streams
export default function Loading() {
return <Skeleton rows={5} />
}
// app/blog/error.js — must be a client component
'use client'
export default function Error({ error, reset }) {
return (
<div role="alert">
<p>Could not load posts: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}loading.jswraps the segment in a Suspense boundary, so the shell renders immediately and the content fills in.error.jscatches render and data errors below it; it receives aresetfunction that retries the segment.not-found.jsrenders whennotFound()is called or a URL matches nothing.global-error.jsis the last resort and must render its ownhtmlandbodyelements.
💡
Special files are per segment, not per app: an
error.js in app/blog/ does not protect the rest of the site. Add one where a failure is plausible, and rely on the root for anything unexpected.FAQ
Do I still need the pages/ router?
No for new work. Pages Router still functions and existing apps can stay on it, but new features land in the App Router, and mixing the two in one project is more confusing than migrating.
How do I link between pages without a full reload?
Import
Link from next/link. It prefetches visible links in production, so navigation is usually instant.Related
Data fetching and server components Components and props
Last refreshed 2026-09-18.