Contents
23 Chapter

Starting Next.js and the App Router

Create a Next.js 15 project and get the App Router's file-based routing and layout system into your hands. We cover the file conventions (page · layout · loading · error · not-found · route group) in one chapter.

Chapter 22 covered the background for why Server Components are needed. This chapter moves into actual code. The goal is to create a Next.js 15 project and learn the App Router’s file-based routing.

The model in this chapter solves the same problem (URL → screen) as Chapter 15 (React Router), but in a different way: through the file system. Keep the comparison table from the end of Chapter 15 in mind as you read and this chapter will feel light.

Creating a Next.js project #

create a new Next.js project
pnpm create next-app@latest modern-react-demo

When prompted, choose as follows (the defaults for Part 4).

setup options
✔ TypeScript? ........ Yes  (TS for everything, per the Part 3 baseline)
✔ ESLint? ............ Yes
✔ Tailwind CSS? ...... No  (this book keeps examples simple with inline styles)
✔ src/ directory? .... Yes
✔ App Router? ........ Yes  (must be Yes)
✔ Turbopack? ......... Yes
✔ import alias? ...... No

The most important is Yes to App Router. App Router is the new router that supports Server Components, and Part 4 is built entirely on App Router.

Note
The older Pages Router system is still part of Next.js, but for a new project the standard is the App Router. The two systems differ in structure and behavior, and Server Components only work properly on the App Router. The procedure for moving from Pages Router to App Router is covered in Appendix A (Migrating old React code).

Once creation finishes, move into the folder and start the dev server.

run the dev server
cd modern-react-demo
pnpm dev

Visit http://localhost:3000 and you will see the default Next.js screen.

Looking around the project #

If this is your first time, parts will feel familiar and parts will feel new. The essentials:

modern-react-demo/
modern-react-demo/
├── public/                ← static files (images, etc.)
├── src/
│   └── app/               ← this is the core. Routing begins here
│       ├── layout.tsx     ← the shared layout for every page
│       ├── page.tsx       ← the page at '/'
│       ├── globals.css    ← global styles
│       └── favicon.ico
├── package.json
├── next.config.ts
└── tsconfig.json

The biggest difference from a Vite project is that the file and folder structure inside src/app/ itself is the routing. Each URL path is a folder, and the page.tsx inside it paints the screen. This is file-based routing.

Compared with Chapter 15’s <Route path="/about" element={<About />} /> model, the decisive difference is that the directory structure, not code, defines routes.

The simplest page #

Clear src/app/page.tsx and write it fresh.

src/app/page.tsx
export default function HomePage() {
  return (
    <main style={{ padding: '24px' }}>
      <h1>Home</h1>
      <p>Welcome to Part 4 of React.</p>
    </main>
  );
}

Save and the / route updates. This component is a Server Component. Without 'use client', that is the default. If you write a console.log in it, the output appears not in the browser but in the dev server terminal.

experiment
export default function HomePage() {
  console.log('where does this log?');  // dev server terminal
  return <h1>Home</h1>;
}

This is the most direct way to confirm that the code runs on the server. We will dig deeper in Chapter 24 (Server vs Client Components).

Adding a new route #

Let’s add an /about page. Make a folder and put a page.tsx inside.

folder structure
src/app/
├── layout.tsx
├── page.tsx              ← '/'
└── about/
    └── page.tsx          ← '/about'

src/app/about/page.tsx:

src/app/about/page.tsx
export default function AboutPage() {
  return (
    <main style={{ padding: '24px' }}>
      <h1>About</h1>
      <p>This site is a learning demo for Part 4 of the modern-react book.</p>
    </main>
  );
}

Visit http://localhost:3000/about and the new page appears. Not a single line of routing configuration was written — folders alone produced the route.

Dynamic routes #

Routes that take a dynamic parameter in the URL are named with the [parameter] folder convention.

dynamic route
src/app/
└── posts/
    └── [slug]/
        └── page.tsx      ← '/posts/anything'

src/app/posts/[slug]/page.tsx:

src/app/posts/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: Props) {
  const { slug } = await params;

  return (
    <main style={{ padding: '24px' }}>
      <h1>Post: {slug}</h1>
      <p>The slug for this page is "{slug}".</p>
    </main>
  );
}

URLs like /posts/hello-world or /posts/intro-to-react all match this file, and you extract the dynamic part with params.slug. Note that from Next.js 15 params is a Promise, so you have to await it.

This is similar to Chapter 15’s useParams, but here it arrives as a prop to the component. That is because hooks cannot be used inside Server Components (covered in detail in Chapter 24).

Moving between pages — <Link> #

Use the Link component that Next.js provides for navigation. A plain <a> would trigger a full page reload, so always use Link for client-side transitions.

src/app/page.tsx
import Link from 'next/link';

export default function HomePage() {
  return (
    <main style={{ padding: '24px' }}>
      <h1>Home</h1>
      <ul>
        <li><Link href="/about">About</Link></li>
        <li><Link href="/posts/hello-world">First post</Link></li>
      </ul>
    </main>
  );
}

Link prefetches each linked page as soon as it appears on screen, so the transition feels instant on click. It plays the same role as Chapter 15’s <Link to=...>, with automatic prefetch as the difference.

Layout — the shared shell #

How do you handle the header, footer, and sidebar — the parts shared across pages? In Next.js the layout.tsx file plays that role.

src/app/layout.tsx (auto-generated for you):

src/app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';

export const metadata = {
  title: 'React demo',
  description: 'For learning Next.js',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header style={{ padding: '12px', background: '#f4f4f4' }}>
          <strong>My site</strong>
        </header>
        <div>{children}</div>
        <footer style={{ padding: '12px', background: '#f4f4f4', marginTop: '40px' }}>
          © 2026
        </footer>
      </body>
    </html>
  );
}

The key points.

  • <html> and <body> must live here (the root layout is the page skeleton)
  • children is the page beneath that layout (or a deeper layout)
  • metadata provides the <head> info. Next.js takes care of it

Now every page gets a header and footer automatically. Each page.tsx only has to write the body.

Nested layouts #

Putting a layout.tsx inside a folder adds a layout that applies only to that folder and its descendants. Layouts nest.

nested layout structure
src/app/
├── layout.tsx              ← shared across all pages (root layout)
├── page.tsx                ← '/'
└── docs/
    ├── layout.tsx          ← applies to every page under '/docs/...'
    ├── page.tsx            ← '/docs'
    └── [slug]/
        └── page.tsx        ← '/docs/anything'

src/app/docs/layout.tsx:

src/app/docs/layout.tsx
import Link from 'next/link';
import type { ReactNode } from 'react';

export default function DocsLayout({ children }: { children: ReactNode }) {
  return (
    <div style={{ display: 'flex', gap: '24px', padding: '24px' }}>
      <aside style={{ width: '180px', borderRight: '1px solid #eee', paddingRight: '16px' }}>
        <h3>Docs</h3>
        <ul>
          <li><Link href="/docs/intro">Getting started</Link></li>
          <li><Link href="/docs/api">API</Link></li>
        </ul>
      </aside>
      <section style={{ flex: 1 }}>
        {children}
      </section>
    </div>
  );
}

Now every page that starts with /docs gets the sidebar automatically. Other routes (/about, /posts/...) are unaffected. Layouts nest naturally along the page tree. It looks like Chapter 15’s <Outlet /> + nested route pattern, automated by the file system.

When you move between pages, the layout itself does not remount — only the parts that changed get re-rendered. So the sidebar’s scroll position is preserved, and you get a smooth UX naturally.

App Router file conventions #

Besides page.tsx and layout.tsx, the App Router has other special files that activate automatically when placed in a folder.

FileRoleWhere it shows up in this book
page.tsxThe screen for the route (required)This chapter
layout.tsxShared layout for that folder and belowThis chapter
loading.tsxSuspense fallbackChapter 26
error.tsxError boundaryPairs with Chapter 33 (Sentry)
not-found.tsx404 pageThis chapter (optional)
route.tsAPI route (an endpoint, not a page)Some of Chapter 27
template.tsxLike layout but remounts on every navigation(Outside the scope of this book)
(group)Route group (a grouping folder that does not appear in the URL)Chapter 34 capstone

No need to memorize these now; just know they exist. They appear in turn through Part 4.

not-found.tsx — 404 #

The screen when no route matches is defined by not-found.tsx.

src/app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>404  Page not found</h1>
      <Link href="/">Go home</Link>
    </div>
  );
}

In place of Chapter 15’s path="*" wildcard route, you handle this via a file-name convention.

Pages Router vs App Router — one-line comparison #

A short comparison with Pages Router, which you will run into in older Next.js projects and articles. The detailed migration procedure is in Appendix A.

ItemPages Router (old)App Router (this book)
Route definitionpages/index.tsxapp/page.tsx
Dynamic routepages/posts/[slug].tsxapp/posts/[slug]/page.tsx
Layout_app.tsx single / hand-composedapp/layout.tsx auto-nested
Data fetchinggetServerSideProps / getStaticPropsServer Component function body (Chapter 25)
API routepages/api/*.tsapp/.../route.ts
Server Components✓ (default)

Part 4 is entirely on App Router.

Putting it together — a small site #

Let’s combine what we have learned into a small site.

the structure we will build
src/app/
├── layout.tsx                     ← header + footer
├── page.tsx                       ← '/'
├── about/page.tsx                 ← '/about'
└── posts/
    ├── page.tsx                   ← '/posts' (list)
    └── [slug]/page.tsx            ← '/posts/[slug]' (detail)

src/app/layout.tsx:

src/app/layout.tsx
import Link from 'next/link';
import type { ReactNode } from 'react';
import './globals.css';

export const metadata = { title: 'React demo' };

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header style={{ padding: '12px 24px', background: '#222', color: '#fff' }}>
          <Link href="/" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>Home</Link>
          <Link href="/about" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>About</Link>
          <Link href="/posts" style={{ color: '#fff', textDecoration: 'none' }}>Posts</Link>
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}

src/app/page.tsx:

src/app/page.tsx
export default function HomePage() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>Home</h1>
      <p>A modern React demo built with Next.js.</p>
    </div>
  );
}

src/app/posts/page.tsx:

src/app/posts/page.tsx
import Link from 'next/link';

const POSTS = [
  { slug: 'hello-world', title: 'First post' },
  { slug: 'about-rsc', title: 'What is RSC?' },
  { slug: 'tips', title: 'Study tips' },
];

export default function PostsPage() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>Posts</h1>
      <ul>
        {POSTS.map(post => (
          <li key={post.slug}>
            <Link href={`/posts/${post.slug}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

src/app/posts/[slug]/page.tsx:

src/app/posts/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  return (
    <div style={{ padding: '24px' }}>
      <h1>{slug}</h1>
      <p>This is the body for the slug "{slug}".</p>
    </div>
  );
}

Save and click the header links. Page transitions happen smoothly without flicker and the URL updates correctly.

Exercises #

  1. Add a nested dynamic route like /posts/[slug]/comments to the mini site above. The folder structure is app/posts/[slug]/comments/page.tsx. The type of params stays Promise<{ slug: string }> (a child path still receives the parent’s dynamic segment as-is).
  2. Create a route group like /docs/(marketing)/landing. Use app/docs/(marketing)/landing/page.tsx. Folders wrapped in parentheses do not appear in the URL but participate in layout grouping. The actual URL becomes /docs/landing.
  3. Place not-found.tsx inside app/posts/ (not at the root) and access /posts/some-missing-slug. (When the slug page calls notFound(), the nearest not-found.tsx is rendered.) Bonus: the import { notFound } from 'next/navigation'; if (!post) notFound(); pattern.

In one line: Next.js 15 + App Router is the environment for Part 4. The folder structure under src/app/ is the routing; page.tsx is the screen and layout.tsx is the shared shell. Dynamic routes use [param] folders, and params is a Promise. <Link> gives client-side transitions plus prefetch. What Chapter 15’s React Router defined in code is automated by the file system.

Next chapter #

Every page we built was a Server Component. But what happens when click events or useState come into play? In the next Chapter 24 Server Components vs Client Components, we make the difference between the two kinds explicit, look at the role of the 'use client' directive, and learn how to mix them together.

X