Skip to content

File Conventions and Special Files

In the App Router, folders define route segments and a small set of reserved filenames give each folder its behavior — a folder is only reachable as a URL once it contains a page.

Folders are segments, special files are behavior

Section titled “Folders are segments, special files are behavior”

A folder under app/ is a URL segment. But a folder on its own is inert; what it does is decided by the reserved files inside it. Each filename has one job:

  • page.tsx — the UI for a route, and the thing that makes the segment publicly routable.
  • layout.tsx — a shared wrapper that persists across navigation and nests inside parent layouts.
  • loading.tsx — a Suspense fallback shown while the segment streams in.
  • error.tsx — an error boundary for the segment; it must be a Client Component.
  • not-found.tsx — the UI rendered when notFound() is called.
  • route.ts — a Route Handler, i.e. an API endpoint instead of a page.
  • template.tsx — like a layout, but it re-mounts a fresh instance on every navigation.
app/
layout.tsx # root layout — wraps everything
page.tsx # "/"
dashboard/
layout.tsx # wraps everything under /dashboard
loading.tsx # fallback while /dashboard loads
error.tsx # catches errors in /dashboard
page.tsx # "/dashboard"
api/
users/
route.ts # "/api/users" — a Route Handler, not a page

A page receives route params and search params and returns the segment’s UI:

app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <article>Reading: {slug}</article>;
}

A layout wraps a segment and everything below it. It keeps its state across navigations between sibling pages, so shared chrome like a sidebar does not remount:

app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>Dashboard nav</nav>
{children}
</section>
);
}

Every App Router app must have one root layout at app/layout.tsx. It is the only layout that renders the <html> and <body> tags, because Next.js does not emit them for you:

app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

Two rules follow from all this. First, a folder without a page.tsx (or a route.ts) is not routable — you can nest folders purely to organize files and they will never respond to a URL. Second, error.tsx must start with "use client", because error boundaries rely on client-side React.

flowchart LR
  Folder[dashboard folder] --> Layout[layout wraps and persists]
  Layout --> Loading[loading is the Suspense fallback]
  Layout --> Error[error catches failures]
  Layout --> Page[page is the routable UI]
  Page --> URL[/dashboard is reachable]
How special files map onto a route segment

page and layout are the two you reach for daily, but the App Router reserves a much longer list of filenames, each verified here against the bundled Next.js docs (node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/):

FileWhat it doesStatus
page.tsxThe UI for a segment; makes it publicly routable.Stable
layout.tsxShared wrapper that persists across sibling navigations; the root one must render <html>/<body>.Stable
template.tsxLike a layout, but re-mounts a fresh instance on every navigation instead of persisting.Stable
loading.tsxA <Suspense> fallback shown while the segment streams in.Stable
error.tsxA Client Component error boundary for the segment.Stable
not-found.tsxUI rendered when notFound() is called, or a segment doesn’t match.Stable
default.tsxFallback UI for a Parallel Routes slot when Next.js can’t recover its active state on a hard navigation.Stable
global-error.tsxRoot-level error boundary — the only one that can catch errors thrown by the root layout/template themselves. Must define its own <html>/<body>, and cannot export metadata.Stable
forbidden.tsxRendered when forbidden() is called; Next.js returns a 403.Experimental
unauthorized.tsxRendered when unauthorized() is called; Next.js returns a 401.Experimental
route.tsA Route Handler — GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS — instead of a page.Stable
proxy.tsRuns before a request completes on every matching route (the file Next.js 16 renamed from middleware.ts); see Proxy and the Request Lifecycle.Stable
instrumentation.tsExports an optional register() (runs once per server instance) and an optional onRequestError() for observability. Root-level only.Stable
instrumentation-client.tsClient-side counterpart — runs before the app becomes interactive, for monitoring/analytics/polyfills. No required exports.Stable
mdx-components.tsxRequired to use @next/mdx with the App Router; exports a single useMDXComponents() function.Stable
sitemap.ts / sitemap.xmlGenerates (or statically defines) /sitemap.xml.Stable
robots.ts / robots.txtGenerates (or statically defines) /robots.txt; the generated form is cached by default like any other Route Handler.Stable
opengraph-image.tsx / twitter-image.tsxGenerates a segment’s social-sharing image with ImageResponse, or ships a static image file of the same name.Stable

forbidden.tsx and unauthorized.tsx are the only two in this list still marked experimental in the bundled docs — everything else, including global-error, instrumentation-client, and the metadata file conventions, is stable in Next.js 16.3.

The component hierarchy is fixed, and it is not the order the files sit in your editor’s file tree — it’s the order React nests them around your page, outermost to innermost:

flowchart TD
  Layout["layout.tsx (outermost)"] --> Template["template.tsx"]
  Template --> Error["error.tsx (error boundary)"]
  Error --> Loading["loading.tsx (Suspense boundary)"]
  Loading --> NotFound["not-found.tsx"]
  NotFound --> Page["page.tsx (innermost)"]
Component nesting order for one route segment

This is stated directly, file by file, in the bundled docs: layout.js “wraps template.js, error.js, loading.js, not-found.js, and page.js”; template.js “renders between layout.js and error.js… wraps error.js, loading.js, not-found.js, and page.js, but does not wrap the layout.js”; error.js “wraps loading.js, not-found.js, page.js, and nested layout.js files… does not wrap the layout.js or template.js above it”; loading.js “wraps not-found.js, page.js, and nested layout.js files in a <Suspense> boundary… does not wrap the layout.js, template.js, or error.js”; and page.js “is the innermost file convention.” One consequence worth internalizing: because error.js sits inside layout.js in this order, an error thrown by the layout itself is never caught by that segment’s own error.tsx — only global-error.tsx (or a parent segment’s error.tsx) can catch it.

A route segment is not “one file” — it’s a fixed nesting of up to six optional wrapper components around your page, in an order the framework enforces rather than one your file tree implies. Reading a folder means asking, for each reserved name present: what layer of that nesting does it occupy, and what does it not reach — a layout never sees the errors of its own template/error children above it, and a loading boundary never covers the layout it hangs from.

What you getBenefitCost
Reserved filenames instead of a router configThe folder tree is the routing table — nothing else to keep in syncYou must memorize which names are meaningful; a typo (Page.tsx) is just an inert file
Fixed nesting order (layout → template → error → loading → not-found → page)Predictable, composable error/loading boundaries per segmentA layout’s own errors are invisible to its segment’s error.tsx — easy to assume otherwise
global-error.tsx for root-level failuresThe only way to catch an error thrown by the root layout itselfMust duplicate its own <html>/<body> and cannot use metadata
forbidden.tsx / unauthorized.tsxPurpose-built 403/401 UI tied to forbidden()/unauthorized()Still experimental — API surface can still change
  • “error.tsx catches errors thrown by its own layout.tsx.” It does not — in the nesting order, error.tsx sits inside layout.tsx, so a layout’s own render errors skip past it. Only global-error.tsx (root) or a parent segment’s error.tsx can catch a layout’s errors.
  • “Any reserved filename works anywhere, at any depth.” Some are root-only (instrumentation.ts, instrumentation-client.ts), and some depend on other conventions being in place (mdx-components.tsx only matters with @next/mdx installed).
  • “forbidden.tsx/unauthorized.tsx are as stable as error.tsx.” They’re both still marked experimental in the bundled docs, unlike the rest of this table.
  • “A folder with files in it is automatically a route.” Only page.tsx (or route.ts) makes a segment reachable — a folder that only holds a layout.tsx, or plain organizational subfolders, never responds to a URL on its own.

When a generated snippet claims a file “just works” because of its name, check for these before trusting it:

  • Is error.tsx expected to catch a failure thrown directly inside the layout.tsx in the same segment? It can’t — that requires global-error.tsx or a parent segment’s error.tsx.
  • Is a root-only convention (instrumentation.ts, instrumentation-client.ts) placed inside a nested route folder instead of the project root (or src/)?
  • Does global-error.tsx skip 'use client', or try to export metadata? Both are required/forbidden respectively, and the file must render its own <html>/<body>.
  • Is forbidden.tsx/unauthorized.tsx presented as a long-stable API, when the bundled docs still mark both experimental?
  • Does a route.ts sit next to a page.tsx in the very same segment, expecting both to work? A segment resolves to one or the other, not both, for the same path.

🐛 Spot the bug

app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const config = getDashboardConfig() // throws if the config file is missing
return (
<section>
<nav>{config.title}</nav>
{children}
</section>
)
}
// app/dashboard/error.tsx
'use client'
export default function DashboardError({ error }: { error: Error }) {
return <p>Dashboard failed to load: {error.message}</p>
}
Show the answer

This looks like a matched pair, but error.tsx sits inside layout.tsx in the nesting order — it wraps page.tsx (and any nested layout.tsx), not the layout.tsx in its own segment. If getDashboardConfig() throws while DashboardLayout itself renders, app/dashboard/error.tsx never sees it; the error propagates up to the nearest parent segment’s error.tsx, or to the root’s global-error.tsx if there is no parent boundary. The fix is either to move the risky call into page.tsx (which this error.tsx does cover), or to rely on the parent segment’s error boundary and design for that intentionally.

Which file makes a route segment publicly reachable at a URL?
What is special about error.tsx?
What lives only in the root layout?
A folder that contains no page.tsx and no route.ts is:
In the nesting order layout → template → error → loading → not-found → page, which statement is correct?
What is wrong with this file as written?
// app/global-error.tsx
export default function GlobalError({ error }: { error: Error }) {
  return <p>Something went wrong: {error.message}</p>
}
Which two file conventions does the bundled Next.js 16.3 documentation still mark experimental?