Next.js App Router Cheat Sheet

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.

The files you will write most
FileDoesMust know
page.tsxMakes the segment a public routeWithout page or route, the URL does not exist
layout.tsxShared UI that keeps state on navigationThe root layout must include html and body
loading.tsxInstant fallback while the segment streamsTakes no props
error.tsxError boundary for the segmentMust start with 'use client'
route.tsAPI 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.

Route path tester

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.

All App Router conventions

Files, folders and metadata files
ConventionKindWhat it does
layoutFileShared UI for a segment and its children; stays mounted
pageFileUI for a route; makes the segment public
loadingFileSuspense fallback for the segment
not-foundFileUI shown for notFound() and unmatched URLs
errorFileError boundary; Client Component
global-errorFileError UI for the root layout; own html and body
routeFileAPI endpoint; .js or .ts only
templateFileLike a layout but re-rendered on navigation
defaultFileFallback for a parallel route slot
forbiddenFileUI for forbidden()
unauthorizedFileUI for unauthorized()
proxy.tsRoot fileRequest proxy at the project root
instrumentation.tsRoot fileOpenTelemetry and instrumentation setup
folderFolderA URL segment
[slug]FolderDynamic segment
[...slug]FolderCatch-all segment, one or more parts
[[...slug]]FolderOptional catch-all, also matches the parent
(group)FolderRoute group, left out of the URL
_folderFolderPrivate folder, ignored by routing
@slotFolderNamed slot for parallel routes
(.)folderFolderIntercept a route at the same level
(..)folderFolderIntercept a route one level up
(..)(..)folderFolderIntercept a route two levels up
(...)folderFolderIntercept a route from the app root
favicon / icon / apple-iconMetadataApp icons, as images or generated with code
opengraph-image / twitter-imageMetadataShare images, as files or generated
sitemap / robotsMetadatasitemap.xml and robots.txt, static or generated

Starter files

app/layout.tsx: the root layout

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>
  );
}

app/blog/[slug]/page.tsx: a dynamic page

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>
  );
}

app/blog/loading.tsx: instant loading state

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>;
}

app/blog/error.tsx: error boundary with retry

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>
  );
}

app/global-error.tsx: errors in the root layout

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>
  );
}

How the special files nest

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.

Dynamic segments and params

[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.

Streaming and status codes

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.

How it works

  1. Search or filter the conventions table by files, folders or metadata.
  2. Type a path such as app/(shop)/blog/[slug]/page.tsx into the path tester to see its URL pattern.
  3. Read the nesting order to understand which file wraps which.
  4. Copy the starter files and rename the segment to fit your app.

Tips

Frequently asked questions

What is the difference between layout and template?

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.

Why does my error.tsx not catch an error?

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.

Which props does error.tsx receive now?

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.

How do I make a URL segment that starts with an underscore?

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.

Where does middleware go in the App Router?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap