File Conventions and Special Files
The idea in one sentence
Section titled “The idea in one sentence”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 whennotFound()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 pagepage and layout are the two you use most
Section titled “page and layout are the two you use most”A page receives route params and search params and returns the segment’s UI:
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:
export default function DashboardLayout({ children,}: { children: React.ReactNode;}) { return ( <section> <nav>Dashboard nav</nav> {children} </section> );}The root layout is required
Section titled “The root layout is required”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:
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]
The full special-files reference
Section titled “The full special-files reference”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/):
| File | What it does | Status |
|---|---|---|
page.tsx | The UI for a segment; makes it publicly routable. | Stable |
layout.tsx | Shared wrapper that persists across sibling navigations; the root one must render <html>/<body>. | Stable |
template.tsx | Like a layout, but re-mounts a fresh instance on every navigation instead of persisting. | Stable |
loading.tsx | A <Suspense> fallback shown while the segment streams in. | Stable |
error.tsx | A Client Component error boundary for the segment. | Stable |
not-found.tsx | UI rendered when notFound() is called, or a segment doesn’t match. | Stable |
default.tsx | Fallback UI for a Parallel Routes slot when Next.js can’t recover its active state on a hard navigation. | Stable |
global-error.tsx | Root-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.tsx | Rendered when forbidden() is called; Next.js returns a 403. | Experimental |
unauthorized.tsx | Rendered when unauthorized() is called; Next.js returns a 401. | Experimental |
route.ts | A Route Handler — GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS — instead of a page. | Stable |
proxy.ts | Runs 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.ts | Exports an optional register() (runs once per server instance) and an optional onRequestError() for observability. Root-level only. | Stable |
instrumentation-client.ts | Client-side counterpart — runs before the app becomes interactive, for monitoring/analytics/polyfills. No required exports. | Stable |
mdx-components.tsx | Required to use @next/mdx with the App Router; exports a single useMDXComponents() function. | Stable |
sitemap.ts / sitemap.xml | Generates (or statically defines) /sitemap.xml. | Stable |
robots.ts / robots.txt | Generates (or statically defines) /robots.txt; the generated form is cached by default like any other Route Handler. | Stable |
opengraph-image.tsx / twitter-image.tsx | Generates 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.
Nesting order
Section titled “Nesting order”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)"]
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.
Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Reserved filenames instead of a router config | The folder tree is the routing table — nothing else to keep in sync | You 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 segment | A layout’s own errors are invisible to its segment’s error.tsx — easy to assume otherwise |
global-error.tsx for root-level failures | The only way to catch an error thrown by the root layout itself | Must duplicate its own <html>/<body> and cannot use metadata |
forbidden.tsx / unauthorized.tsx | Purpose-built 403/401 UI tied to forbidden()/unauthorized() | Still experimental — API surface can still change |
Common Misconceptions
Section titled “Common Misconceptions”- “
error.tsxcatches errors thrown by its ownlayout.tsx.” It does not — in the nesting order,error.tsxsits insidelayout.tsx, so a layout’s own render errors skip past it. Onlyglobal-error.tsx(root) or a parent segment’serror.tsxcan 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.tsxonly matters with@next/mdxinstalled). - “
forbidden.tsx/unauthorized.tsxare as stable aserror.tsx.” They’re both still markedexperimentalin the bundled docs, unlike the rest of this table. - “A folder with files in it is automatically a route.” Only
page.tsx(orroute.ts) makes a segment reachable — a folder that only holds alayout.tsx, or plain organizational subfolders, never responds to a URL on its own.
Reading AI-generated code
Section titled “Reading AI-generated code”When a generated snippet claims a file “just works” because of its name, check for these before trusting it:
- Is
error.tsxexpected to catch a failure thrown directly inside thelayout.tsxin the same segment? It can’t — that requiresglobal-error.tsxor a parent segment’serror.tsx. - Is a root-only convention (
instrumentation.ts,instrumentation-client.ts) placed inside a nested route folder instead of the project root (orsrc/)? - Does
global-error.tsxskip'use client', or try to exportmetadata? Both are required/forbidden respectively, and the file must render its own<html>/<body>. - Is
forbidden.tsx/unauthorized.tsxpresented as a long-stable API, when the bundled docs still mark bothexperimental? - Does a
route.tssit next to apage.tsxin 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
export default function DashboardLayout({ children }: { children: React.ReactNode }) {const config = getDashboardConfig() // throws if the config file is missingreturn ( <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.