Modern React + Next.js #2 Getting Started with Next.js and the App Router

8 min read

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 #

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

When prompted, choose the following (used throughout this series).

Setup options
✔ TypeScript? ........ No
✔ ESLint? ............ Yes
✔ Tailwind CSS? ...... No
✔ src/ directory? .... Yes
✔ App Router? ........ Yes  (must be Yes!)
✔ Turbopack? ......... Yes
✔ import alias? ...... No

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

Note
The older Pages Router still exists in Next.js, but for new projects the App Router is the standard. The two have different mental models, and Server Components only work properly in the App Router.

Once it’s created, enter the folder and start the dev server.

Run the dev server
cd modern-react-demo
npm run dev

Open 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/
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.json

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

src/app/page.js
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.

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

Folder layout
src/app/
├── layout.js
├── page.js              ← '/'
└── about/
    └── page.js          ← '/about'

src/app/about/page.js:

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

Dynamic route
src/app/
└── posts/
    └── [slug]/
        └── page.js      ← '/posts/anything'

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

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.

src/app/page.js
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):

src/app/layout.js
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)
  • children is the page (or further nested layout) underneath this layout
  • metadata becomes 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.

Nested layout structure
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:

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.

FileRole
page.jsthe route’s screen (required)
layout.jsshared layout for that folder and below
loading.jsSuspense fallback (covered in #5)
error.jserror boundary
not-found.js404 screen
route.jsAPI route (an endpoint, not a page)
template.jssimilar 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.

Structure to build
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:

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:

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:

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:

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:

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-app to 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.

X