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 #
pnpm create next-app@latest modern-react-demoWhen prompted, choose as follows (the defaults for Part 4).
✔ 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? ...... NoThe 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.
Once creation finishes, move into the folder and start the dev server.
cd modern-react-demo
pnpm devVisit 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/
├── 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.jsonThe 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.
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.
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.
src/app/
├── layout.tsx
├── page.tsx ← '/'
└── about/
└── page.tsx ← '/about'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.
src/app/
└── posts/
└── [slug]/
└── page.tsx ← '/posts/anything'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.
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):
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)childrenis the page beneath that layout (or a deeper layout)metadataprovides 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.
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:
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.
| File | Role | Where it shows up in this book |
|---|---|---|
page.tsx | The screen for the route (required) | This chapter |
layout.tsx | Shared layout for that folder and below | This chapter |
loading.tsx | Suspense fallback | Chapter 26 |
error.tsx | Error boundary | Pairs with Chapter 33 (Sentry) |
not-found.tsx | 404 page | This chapter (optional) |
route.ts | API route (an endpoint, not a page) | Some of Chapter 27 |
template.tsx | Like 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.
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.
| Item | Pages Router (old) | App Router (this book) |
|---|---|---|
| Route definition | pages/index.tsx | app/page.tsx |
| Dynamic route | pages/posts/[slug].tsx | app/posts/[slug]/page.tsx |
| Layout | _app.tsx single / hand-composed | app/layout.tsx auto-nested |
| Data fetching | getServerSideProps / getStaticProps | Server Component function body (Chapter 25) |
| API route | pages/api/*.ts | app/.../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.
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:
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:
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:
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:
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 #
- Add a nested dynamic route like
/posts/[slug]/commentsto the mini site above. The folder structure isapp/posts/[slug]/comments/page.tsx. The type ofparamsstaysPromise<{ slug: string }>(a child path still receives the parent’s dynamic segment as-is). - Create a route group like
/docs/(marketing)/landing. Useapp/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. - Place
not-found.tsxinsideapp/posts/(not at the root) and access/posts/some-missing-slug. (When the slug page callsnotFound(), the nearestnot-found.tsxis rendered.) Bonus: theimport { 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.tsxis the screen andlayout.tsxis the shared shell. Dynamic routes use[param]folders, andparamsis 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.