Modern React + Next.js #2 Getting Started with Next.js and the App Router
Last time we covered why Server Components exist. This time we get our hands on real code. The goal is to create a Next.js project and learn the App Router’s file-based routing.
Creating a Next.js project #
npx create-next-app@latest modern-react-demoWhen prompted, choose the following (used throughout this series).
✔ TypeScript? ........ No
✔ ESLint? ............ Yes
✔ Tailwind CSS? ...... No
✔ src/ directory? .... Yes
✔ App Router? ........ Yes (must be Yes!)
✔ Turbopack? ......... Yes
✔ import alias? ...... NoThe most important one is selecting Yes for App Router. The App Router is the new router that supports Server Components, and this entire series is built on it.
Once it’s created, enter the folder and start the dev server.
cd modern-react-demo
npm run devOpen http://localhost:3000 and you’ll see the Next.js default page.
Tour of the project structure #
If you’re new to Next.js, some of this will look familiar and some won’t. Here are the key parts:
modern-react-demo/
├── public/ ← static files (images, etc.)
├── src/
│ └── app/ ← this is the heart! Routing starts here
│ ├── layout.js ← shared layout for every page
│ ├── page.js ← page for '/'
│ ├── globals.css ← global styles
│ └── favicon.ico
├── package.json
├── next.config.mjs
└── jsconfig.jsonThe biggest difference from a Vite project is that the file and folder structure inside src/app/ defines the routing. Each URL path maps to a folder, and the page.js inside it renders the screen. This is file-based routing.
The simplest possible page #
Empty out src/app/page.js and rewrite it.
export default function HomePage() {
return (
<main style={{ padding: '24px' }}>
<h1>Home page</h1>
<p>Welcome to the Modern React series.</p>
</main>
);
}Save and the / route updates. This component is a Server Component — without 'use client', that’s the default. If you drop a console.log into it, the output appears in the dev server terminal, not the browser console.
export default function HomePage() {
console.log('Where does this print?'); // The dev server terminal!
return <h1>Home page</h1>;
}That’s a direct way to confirm this code runs on the server. We’ll go deeper in #3.
Adding a new route #
Let’s create an /about page. Make a folder and put a page.js inside it — that’s it.
src/app/
├── layout.js
├── page.js ← '/'
└── about/
└── page.js ← '/about'src/app/about/page.js:
export default function AboutPage() {
return (
<main style={{ padding: '24px' }}>
<h1>About</h1>
<p>This site is a Next.js learning demo.</p>
</main>
);
}Visit http://localhost:3000/about and the new page is there. No routing config was needed — the folder structure alone created the route.
Dynamic routes #
Routes that take a dynamic URL parameter are created with folder names like [parameter].
src/app/
└── posts/
└── [slug]/
└── page.js ← '/posts/anything'src/app/posts/[slug]/page.js:
export default async function PostPage({ params }) {
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 and /posts/intro-to-react all match this file, and you read the dynamic part with params.slug. Note that params is a Promise, so you have to await it (this changed in Next.js 15).
It’s similar to React Router’s useParams from #15, but here it comes in as component props. That’s because hooks can’t be used inside Server Components (we’ll cover this in detail in #3).
Navigating with <Link>
#
Use Next.js’s Link component to navigate between pages. A plain <a> triggers 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 page</h1>
<ul>
<li><Link href="/about">About</Link></li>
<li><Link href="/posts/hello-world">First post</Link></li>
</ul>
</main>
);
}Link even prefetches a page once it appears on screen, so clicking it transitions instantly.
Layout — the shared shell #
How do you handle parts shared by every page, like a header, footer, or sidebar? In Next.js, the layout.js file plays that role.
src/app/layout.js (already auto-generated):
import './globals.css';
export const metadata = {
title: 'Modern React Demo',
description: 'Next.js learning demo',
};
export default function RootLayout({ children }) {
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>
);
}Key points:
<html>and<body>belong here (the root layout is the page’s skeleton)childrenis the page (or further nested layout) underneath this layoutmetadatabecomes the<head>info — Next.js handles it for you
Now every page automatically gets the header and footer. Each page.js only needs to write the body content.
Nested layouts #
If you put a layout.js inside a folder, you add a layout that applies only to that folder and its subroutes. Layouts nest.
src/app/
├── layout.js ← shared by every page (root layout)
├── page.js ← '/'
└── docs/
├── layout.js ← applies to all '/docs/...' pages
├── page.js ← '/docs'
└── [slug]/
└── page.js ← '/docs/anything'src/app/docs/layout.js:
import Link from 'next/link';
export default function DocsLayout({ children }) {
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 under /docs automatically gets the sidebar. Other routes (/about, /posts/...) are unaffected. Layouts nest naturally along the page tree.
When you navigate between pages, the layout itself doesn’t remount — only the changed part re-renders. That gives you a smooth UX (like preserving the sidebar’s scroll position) without any extra work.
Special files — at a glance #
Beyond page.js and layout.js, the App Router has other special files that activate automatically when placed in a folder.
| File | Role |
|---|---|
page.js | the route’s screen (required) |
layout.js | shared layout for that folder and below |
loading.js | Suspense fallback (covered in #5) |
error.js | error boundary |
not-found.js | 404 screen |
route.js | API route (an endpoint, not a page) |
template.js | similar to layout but remounts every time |
You don’t have to memorize all of these now — just know they exist. We’ll meet them one by one as the series progresses.
Hands-on — building a small site #
Let’s combine everything we’ve learned into a small site.
src/app/
├── layout.js ← header + footer
├── page.js ← '/'
├── about/page.js ← '/about'
└── posts/
├── page.js ← '/posts' (list)
└── [slug]/page.js ← '/posts/[slug]' (detail)src/app/layout.js:
import Link from 'next/link';
import './globals.css';
export const metadata = {
title: 'Modern React Demo',
};
export default function RootLayout({ children }) {
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.js:
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/about/page.js:
export default function AboutPage() {
return (
<div style={{ padding: '24px' }}>
<h1>About</h1>
<p>This site is a learning demo.</p>
</div>
);
}src/app/posts/page.js:
import Link from 'next/link';
const POSTS = [
{ slug: 'hello-world', title: 'First post' },
{ slug: 'about-rsc', title: 'What is RSC?' },
{ slug: 'tips', title: 'Learning 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.js:
export default async function PostPage({ params }) {
const { slug } = await params;
return (
<div style={{ padding: '24px' }}>
<h1>{slug}</h1>
<p>This page is the body for slug "{slug}".</p>
</div>
);
}Save and click the links in the header. The screen transitions smoothly without any flash, and the URL updates correctly.
Wrap-up #
In this post we covered the start of Next.js and the heart of the App Router.
- Use
npx create-next-appto bootstrap (App Router required) - The folder structure under
src/app/is the routing page.js= the screen,layout.js= the shared shell- Dynamic routes use
[param]folder names <Link>for client-side transitions- Layouts nest naturally
Every page we’ve built so far has been a Server Component. But what happens when click handlers or useState-style interactions enter the picture? In the next post, “Modern React + Next.js #3 Server Components vs Client Components,” we’ll clarify the difference between the two component types, the role of the 'use client' directive, and how to mix them.