In the App Router, folders inside app/ become URL segments and special files give each segment its UI: page makes it public, layout wraps it, loading adds a Suspense fallback, error adds an error boundary and route serves an API. Brackets make segments dynamic, parentheses group routes without changing the URL, and an underscore hides a folder.
| File | Does | Must know |
|---|---|---|
| page.tsx | Makes the segment a public route | Without page or route, the URL does not exist |
| layout.tsx | Shared UI that keeps state on navigation | The root layout must include html and body |
| loading.tsx | Instant fallback while the segment streams | Takes no props |
| error.tsx | Error boundary for the segment | Must start with 'use client' |
| route.ts | API endpoint (Route Handler) | Only .js or .ts |
Updated
Forum threads about the App Router mostly ask where a file goes and why it is not picked up. This sheet answers that directly: search the conventions, type a file path to see the URL it produces, and copy minimal versions of the files, all checked against the current nextjs.org documentation.
Type a file path inside app/. The tester drops route groups, reads dynamic segments and tells you the URL pattern, or why the file is not a route.
app/page.tsx: Page at /app/(shop)/cart/page.tsx: Page at /cart. (shop) is a route group, left out of the URL.app/blog/[slug]/page.tsx: Page at /blog/:slugapp/docs/[[...slug]]/page.tsx: Page at /docs/:slug*. [[...slug]] also matches the parent path.app/api/posts/route.ts: API endpoint at /api/postsapp/blog/_components/Post.tsx: Not a route: a folder starting with _ is private.| Convention | Kind | What it does |
|---|---|---|
| layout | File | Shared UI for a segment and its children; stays mounted |
| page | File | UI for a route; makes the segment public |
| loading | File | Suspense fallback for the segment |
| not-found | File | UI shown for notFound() and unmatched URLs |
| error | File | Error boundary; Client Component |
| global-error | File | Error UI for the root layout; own html and body |
| route | File | API endpoint; .js or .ts only |
| template | File | Like a layout but re-rendered on navigation |
| default | File | Fallback for a parallel route slot |
| forbidden | File | UI for forbidden() |
| unauthorized | File | UI for unauthorized() |
| proxy.ts | Root file | Request proxy at the project root |
| instrumentation.ts | Root file | OpenTelemetry and instrumentation setup |
| folder | Folder | A URL segment |
| [slug] | Folder | Dynamic segment |
| [...slug] | Folder | Catch-all segment, one or more parts |
| [[...slug]] | Folder | Optional catch-all, also matches the parent |
| (group) | Folder | Route group, left out of the URL |
| _folder | Folder | Private folder, ignored by routing |
| @slot | Folder | Named slot for parallel routes |
| (.)folder | Folder | Intercept a route at the same level |
| (..)folder | Folder | Intercept a route one level up |
| (..)(..)folder | Folder | Intercept a route two levels up |
| (...)folder | Folder | Intercept a route from the app root |
| favicon / icon / apple-icon | Metadata | App icons, as images or generated with code |
| opengraph-image / twitter-image | Metadata | Share images, as files or generated |
| sitemap / robots | Metadata | sitemap.xml and robots.txt, static or generated |
Required, and the only place html and body are written. Every page renders where children goes.
export const metadata = { title: "My site" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>My site</header>
<main>{children}</main>
</body>
</html>
);
}params is a Promise, so the page is async and awaits it before using slug.
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug); // your own data function
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}Shown straight away on navigation while the page streams in. It receives no props.
export default function Loading() {
return <p aria-live="polite">Loading posts...</p>;
}Must be a Client Component. retry re-fetches and re-renders the segment.
"use client";
export default function BlogError({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
return (
<div role="alert">
<h2>Could not load the blog.</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}Replaces the root layout while active, so it brings its own html and body and cannot export metadata.
"use client";
export default function GlobalError({ retry }: { retry: () => void }) {
return (
<html lang="en">
<body>
<h2>Something went wrong.</h2>
<button onClick={() => retry()}>Try again</button>
</body>
</html>
);
}
Inside one segment the order from outside in is layout, template, error, loading, not-found, then the page or a nested layout. A child segment's set sits inside its parent's.
That order explains most surprises. loading cannot cover a slow layout in the same folder, and error cannot catch that layout's crash, because both sit inside it.
[slug] matches one segment, [...slug] matches one or more, and [[...slug]] also matches the bare parent path. The value arrives on the params prop.
Generated PageProps and LayoutProps helper types infer params from the route string, so PageProps<'/blog/[slug]'> types slug for you after next dev, next build or next typegen.
A streamed page starts with a 200 status. Calling notFound before streaming begins keeps the 404; afterwards Next.js adds a noindex robots tag instead.
Keep slow, uncached data out of layouts, or wrap it in its own Suspense boundary, so loading screens show instantly.
A layout stays mounted when you move between its child pages, so state and effects persist. A template file sits just inside the layout and is re-rendered on navigation, which suits effects that should run on every page change.
Either it is missing the 'use client' line, since error boundaries must be Client Components, or the error happens in the layout of the same segment, which error.tsx does not wrap. Errors in the root layout need global-error.
error, an Error object with an optional digest, and retry, which re-fetches and re-renders the segment. The docs say retry became stable in version 16.3. reset is still there for clearing the error without re-fetching, but retry is the usual choice.
An underscore folder is private and ignored by routing. To get a real URL segment starting with an underscore, name the folder with %5F, the encoded form, for example %5Fdrafts.
In version 16 the middleware file convention was deprecated and renamed to proxy, so new projects use proxy.ts at the project root (or inside src) exporting a proxy function. Next.js ships a codemod, middleware-to-proxy, that renames the file and the function.
Related: Next.js middleware examples, Next.js Route Handlers examples, React Server Components, Next.js Image component, Next.js SEO checklist.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.