AnyLearn
All lessons
Programmingintermediate

Next.js App Router Fundamentals

Next.js 15 App Router: page/layout/loading/error files, nested layouts, dynamic and catch-all segments, route groups, parallel routes, and the Metadata API for SEO — with the file tree as a diagram.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 10

App Router in one paragraph

In Next.js 15 the App Router (the app/ directory) is the path forward — the Pages Router (pages/) still works but stops getting new features. Routing is file-based: every folder under app/ becomes a URL segment, and special filenames inside each folder describe what to render. page.tsx is the page, layout.tsx wraps it, loading.tsx is the Suspense fallback, error.tsx is the boundary, not-found.tsx is the 404 for that segment.

Compared to the Pages Router, App Router is React Server Components by default, supports nested layouts, streams HTML, and gives you Server Actions. In 2026 if you're starting fresh, start with App Router.

Full lesson text

All 10 steps on one page — for reading, reference, and search.

Show

1. App Router in one paragraph

In Next.js 15 the App Router (the app/ directory) is the path forward — the Pages Router (pages/) still works but stops getting new features. Routing is file-based: every folder under app/ becomes a URL segment, and special filenames inside each folder describe what to render. page.tsx is the page, layout.tsx wraps it, loading.tsx is the Suspense fallback, error.tsx is the boundary, not-found.tsx is the 404 for that segment.

Compared to the Pages Router, App Router is React Server Components by default, supports nested layouts, streams HTML, and gives you Server Actions. In 2026 if you're starting fresh, start with App Router.

2. A typical app/ file tree

Each folder is a URL segment. Special filenames decide what gets rendered at each level.

flowchart TD
  A["app/"] --> B["layout.tsx (root)"]
  A --> C["page.tsx (home)"]
  A --> D["loading.tsx"]
  A --> E["error.tsx"]
  A --> F["(marketing)/"]
  F --> G["pricing/page.tsx"]
  A --> H["dashboard/"]
  H --> I["layout.tsx (dashboard chrome)"]
  H --> J["page.tsx"]
  H --> K["[slug]/page.tsx"]

3. page.tsx: the leaf

A page.tsx is what renders for that exact URL. It's a React component — by default a Server Component (we cover that in the next lesson). Default export wins.

// app/dashboard/page.tsx
export default function DashboardPage() {
  return <h1>Dashboard</h1>;
}

For dynamic segments, the page receives params (and searchParams if present) as props:

// app/posts/[slug]/page.tsx
export default async function PostPage({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;        // Next 15 makes params async
  return <article>Post: {slug}</article>;
}

Next 15 made params and searchParams Promises so they can be cached and streamed independently of the rest of the page.

4. layout.tsx: the wrapper

A layout.tsx wraps every page below it. Layouts persist across navigation within their subtree — they don't unmount when only the inner segment changes, so any state in client portions of the layout (sidebars, nav) survives.

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[240px_1fr]">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

Layouts nest. Visiting /dashboard/teams/42 renders app/layout.tsx (root) → app/dashboard/layout.tsxapp/dashboard/teams/[id]/page.tsx. The root app/layout.tsx is special — it owns the <html> and <body> tags and must exist.

5. loading.tsx and error.tsx

These two file conventions wire Suspense and error boundaries automatically around the page below them.

// app/dashboard/loading.tsx — shown while the segment streams in
export default function Loading() { return <Skeleton />; }
// app/dashboard/error.tsx — MUST be a Client Component
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Drop one in any segment and it just works. not-found.tsx is similar — render it manually with notFound() from next/navigation to bail out into a 404 from inside a server component.

6. Dynamic, catch-all, and optional segments

Folder name conventions encode the dynamic shape of the URL:

  • [slug] — one path segment, params.slug = 'hello' for /posts/hello.
  • [...segments] — catch-all, params.segments = ['a','b'] for /docs/a/b.
  • [[...segments]] — optional catch-all, also matches /docs with params.segments undefined.

For build-time prerendering of dynamic routes, export generateStaticParams:

export async function generateStaticParams() {
  const posts = await db.posts.list();
  return posts.map(p => ({ slug: p.slug }));
}

Next will prerender one HTML file per returned param at build time and ISR-revalidate as configured.

7. Route groups: (group) folders

Wrapping a folder name in parentheses creates a route group that does NOT appear in the URL. Use it to share a layout among unrelated pages, or to keep the file tree tidy.

app/
  (marketing)/
    layout.tsx        // applies to pricing + about
    pricing/page.tsx  // -> /pricing
    about/page.tsx    // -> /about
  (app)/
    layout.tsx        // authed shell
    dashboard/page.tsx // -> /dashboard

Marketing pages and the authed app share nothing — different layouts, different fonts, maybe different auth — but you don't want to invent fake URL prefixes for them. Route groups are how you express that organizational split without leaking into URLs.

8. Parallel routes and intercepting routes (briefly)

Two more advanced primitives worth knowing the names of:

  • Parallel routes — folders prefixed with @, e.g. @analytics and @team, render in parallel slots inside the same layout. Useful for dashboards with independent panes that each have their own loading and error state.
  • Intercepting routes — folders prefixed with (.), (..), (...), render one route from inside another's URL (e.g., a photo modal that overlays the feed but has its own shareable URL).

You won't reach for these on day one. When you do, the file-system conventions stay the same: just more special folder names. The mental model — files are routes, folders are segments — doesn't change.

9. Metadata and SEO

Static metadata is a plain export from any layout.tsx or page.tsx:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Dashboard | AnyLearn',
  description: 'Your learning dashboard',
};

Dynamic metadata uses generateMetadata and runs on the server with access to the same params/data:

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params;
  const post = await db.posts.bySlug(slug);
  return {
    title: post.title,
    openGraph: { images: [post.coverUrl] },
  };
}

Next composes the metadata from every layout up the tree, child wins on conflicts. No more <Helmet>, no more dangerouslySetInnerHTML for <title>.

10. App Router vs Pages Router, briefly

If you're maintaining a Pages Router app, here is the short version:

  • Routing: pages/post/[id].tsx becomes app/post/[id]/page.tsx.
  • Data fetching: getServerSideProps / getStaticProps become just await inside a Server Component.
  • Layouts: persistent layouts that used to need a custom _app.tsx pattern are now native via layout.tsx.
  • API routes: pages/api/foo.ts becomes app/api/foo/route.ts with named exports per method (GET, POST, ...).
  • Streaming + Suspense: only the App Router does it cleanly.

Don't migrate page-by-page randomly; do it segment-by-segment. The two routers can coexist in one repo while you move.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Which file convention renders the Suspense fallback for a route segment in the App Router?
    • loading.tsx
    • fallback.tsx
    • skeleton.tsx
    • page.loading.tsx
  2. You want to share a layout between /pricing and /about WITHOUT adding any URL prefix. What do you use?
    • A route group folder like (marketing)/.
    • A dynamic segment [marketing]/.
    • A parallel route slot @marketing/.
    • Symlinks between the two page directories.
  3. In Next.js 15, how do you read params inside an async Server Component page?
    • Await it: `const { slug } = await params` because params is a Promise.
    • Destructure directly: `const { slug } = params`.
    • Call useParams() from next/navigation.
    • Read from process.env.NEXT_PARAMS.
  4. Where do dynamic metadata (e.g., post-title) for a `/posts/[slug]` page belong?
    • An exported `generateMetadata` function in app/posts/[slug]/page.tsx.
    • A meta object in the React component's JSX.
    • next.config.js under metadata.posts.
    • An <Head> import from next/head.
  5. Which is the BEST reason to start a new 2026 Next.js project on the App Router rather than the Pages Router?
    • Server Components, nested layouts, Server Actions, and streaming are App Router only.
    • Pages Router doesn't support TypeScript anymore.
    • Pages Router has been removed from Next.js 15.
    • API routes only work in the App Router.

Related lessons