目次
23 章

Next.js の開始と App Router

Next.js 15 プロジェクトを作り、App Router のファイルベースルーティングと layout システムを手に馴染ませます。ファイル規約(page・layout・loading・error・not-found・route group)を一度に扱います。

第22章で Server Components がなぜ必要なのか、その背景を扱いました。本章では実際に手に取れるコードに入ります。Next.js 15 プロジェクトを作り、App Router のファイルベースルーティングを身につけること が目標です。

本章のモデルは第15章(React Router)のクライアントサイドルーティングと同じ問題(URL → 画面)を、異なる方式、つまりファイルシステムベースで解きます。第15章末尾の比較表を頭に置いて読むと、軽く追っていけます。

Next.js プロジェクトの作成 #

新しい Next.js プロジェクトを作る
pnpm create next-app@latest modern-react-demo

質問が出てきたら次のように選択します(本第4部基準)。

設定オプション
✔ TypeScript? ........ Yes  (第3部で扱った基準ですべてのコードを TS)
✔ ESLint? ............ Yes
✔ Tailwind CSS? ...... No  (本書は例の簡潔さのためインラインスタイル)
✔ src/ directory? .... Yes
✔ App Router? ........ Yes  (必ず Yes)
✔ Turbopack? ......... Yes
✔ import alias? ...... No

最も重要なのは App Router を Yes で選ぶ ことです。App Router が Server Components をサポートする新しいルーターであり、この第4部はすべて App Router ベースです。

注記
以前からある Pages Router というシステムも依然として Next.js に残っていますが、新規プロジェクトなら App Router を使うのが標準 です。2 つのシステムは構造と動作方式が異なり、Server Components は App Router でのみ正しく動作します。Pages Router から App Router に移る手順は付録 A(旧 React マイグレーション)で扱います。

作成が終わったらフォルダに入り、dev サーバーを立ち上げます。

dev サーバーの実行
cd modern-react-demo
pnpm dev

http://localhost:3000 にアクセスすると、Next.js のデフォルト画面が見えます。

プロジェクト構造を見てみる #

初めて見る方には馴染みのある部分と見慣れない部分が混在しているはずです。要点だけ整理すると以下のとおりです。

modern-react-demo/
modern-react-demo/
├── public/                ← 静的ファイル(画像など)
├── src/
│   └── app/               ← ここが核心。ルーティングが始まる場所
│       ├── layout.tsx     ← すべてのページの共通レイアウト
│       ├── page.tsx       ← '/' パスのページ
│       ├── globals.css    ← グローバルスタイル
│       └── favicon.ico
├── package.json
├── next.config.ts
└── tsconfig.json

Vite プロジェクトとの最大の違いは、src/app/ フォルダのファイルとフォルダ構造自体がルーティングになる という点です。URL パスの 1 つひとつがフォルダで、その中の page.tsx が画面を描画します。これが ファイルベースルーティング です。

第15章(React Router)の <Route path="/about" element={<About />} /> モデルと比較すると、ルート定義をコードではなく ディレクトリ構造 が担うのが決定的な違いです。

最も単純なページ #

src/app/page.tsx を空にして書き直してみます。

src/app/page.tsx
export default function HomePage() {
  return (
    <main style={{ padding: '24px' }}>
      <h1>ホームページ</h1>
      <p>モダン React 4部へようこそ。</p>
    </main>
  );
}

保存すると / パスが更新されます。このコンポーネントは Server Component です。'use client' がなければ、デフォルトでそうなるからです。コンソールや dev サーバーターミナルに console.log を書いてみると、その出力は ブラウザではなく dev サーバー側に 表示されます。

実験
export default function HomePage() {
  console.log('これはどこに出力される?');  // dev サーバーターミナル
  return <h1>ホームページ</h1>;
}

サーバーで実行されるコードであることがこのように直感的に確認できます。詳細は続く第24章(Server vs Client Components)で見ます。

新しいルートを追加する #

/about ページを作ってみます。フォルダを作り、その中に page.tsx を置けば終わりです。

フォルダ構造
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>紹介</h1>
      <p>本サイトは modern-react 本第4部の学習用デモです。</p>
    </main>
  );
}

http://localhost:3000/about にアクセスすると、新しいページが見えます。ルーティング設定のコードは一行も書いていないのに、フォルダだけでルーティングができてしまいました。

動的ルート #

URL に動的なパラメータが入るルートは、フォルダ名を [parameter] 形式にして作ります。

動的ルート
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>ポスト: {slug}</h1>
      <p>このページのスラッグは "{slug}" です。</p>
    </main>
  );
}

/posts/hello-world/posts/intro-to-react のような URL がすべてこのファイルにマッチし、params.slug で動的な部分を取り出します。Next.js 15 から params が Promise なので、await を経由する必要がある点に注意してください。

第15章の useParams に似ていますが、ここでは コンポーネントの props として 入ってきます。Server Component の中ではフックを使えないからです(第24章で詳しく扱います)。

リンクで移動する — <Link> #

ページ間の移動は Next.js が提供する Link コンポーネントで行います。通常の <a> はページの再読み込みを引き起こすため、クライアントサイド遷移のために必ず Link を使ってください。

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

export default function HomePage() {
  return (
    <main style={{ padding: '24px' }}>
      <h1>ホームページ</h1>
      <ul>
        <li><Link href="/about">紹介</Link></li>
        <li><Link href="/posts/hello-world">最初のポスト</Link></li>
      </ul>
    </main>
  );
}

Link は画面に見え始めるとそのページを prefetch まで行い、クリックの瞬間に遷移できるようにしてくれます。第15章の <Link to=...> と同じ役割ですが、prefetch まで自動な点が違いです。

Layout — 共通の殻 #

Web サイトのヘッダー、フッター、サイドバーのように 複数ページが共有する部分 をどう扱うか。Next.js では layout.tsx ファイルがその役割を担います。

src/app/layout.tsx(すでに自動生成されています):

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

export const metadata = {
  title: 'モダン React デモ',
  description: 'Next.js 学習用',
};

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

要点。

  • <html><body> はここに置く(root layout がページの骨格)
  • children はその layout の下のページ(またはさらに下の layout)
  • metadata<head> の情報。Next.js がよろしく処理してくれる

これでどのページにもヘッダーとフッターが自動的に付きます。各 page.tsx は本文部分だけ書けばよいわけです。

ネストされた layout #

フォルダに layout.tsx を置くと、そのフォルダと配下のパスにのみ適用される layout を追加できます。layout がネストされるのです。

ネストされた layout 構造
src/app/
├── layout.tsx              ← すべてのページ共通(root layout)
├── page.tsx                ← '/'
└── docs/
    ├── layout.tsx          ← '/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>ドキュメント</h3>
        <ul>
          <li><Link href="/docs/intro">はじめに</Link></li>
          <li><Link href="/docs/api">API</Link></li>
        </ul>
      </aside>
      <section style={{ flex: 1 }}>
        {children}
      </section>
    </div>
  );
}

これで /docs で始まるすべてのページに自動でサイドバーが付きます。他のパス(/about/posts/...)には影響しません。layout がページツリーに沿って自然にネストされる わけです。第15章の <Outlet /> + ネストされたルートのパターンが、ファイルシステムで自動化された形です。

ページ間を移動するとき、layout 自体は 再マウントされず、変更された部分だけが再描画されます。なのでサイドバーのスクロール位置のようなものが保持され、滑らかな UX が自然に出てきます。

App Router のファイル規約まとめ #

App Router には page.tsxlayout.tsx 以外にも、フォルダに置くと自動的に動作する特別なファイルがあります。

ファイル役割本書で扱う位置
page.tsxルートの画面(必須)本章
layout.tsxそのフォルダ以下の共通レイアウト本章
loading.tsxSuspense fallback第26章
error.tsxエラー境界第33章(Sentry)と組
not-found.tsx404 画面本章(オプション)
route.tsAPI ルート(ページではなくエンドポイント)第27章で一部
template.tsxlayout と似ているが毎回再マウントされる版(本書範囲外)
(group)route group(URL に出ないグルーピングフォルダ)第34章キャップストーン

今すぐ覚える必要はなく、「こういうものがある」程度に知っておけばよいです。この第4部を進める中で順番に登場します。

not-found.tsx — 404 #

マッチするルートがないときの画面は not-found.tsx で定義します。

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

export default function NotFound() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>404  ページが見つかりません</h1>
      <Link href="/">ホームへ</Link>
    </div>
  );
}

第15章の path="*" ワイルドカードルートの代わりに、ファイル名規約で処理します。

Pages Router vs App Router — 一行比較 #

旧 Next.js プロジェクトや資料で出会う Pages Router との比較を短く置いておきます。詳細なマイグレーション手順は付録 A で扱います。

項目Pages Router(旧)App Router(本書)
ルート定義pages/index.tsxapp/page.tsx
動的ルートpages/posts/[slug].tsxapp/posts/[slug]/page.tsx
レイアウト_app.tsx 単一 / 直接合成app/layout.tsx 自動ネスト
データフェッチgetServerSideProps / getStaticPropsServer Component 関数本体(第25章)
API ルートpages/api/*.tsapp/.../route.ts
Server Components✓(デフォルト)

この第4部はすべて App Router 基準です。

動作確認 — 小さなサイトを作る #

これまで学んだことを総合して、小さなサイトを作ってみます。

作る構造
src/app/
├── layout.tsx                     ← ヘッダー + フッター
├── page.tsx                       ← '/'
├── about/page.tsx                 ← '/about'
└── posts/
    ├── page.tsx                   ← '/posts'(一覧)
    └── [slug]/page.tsx            ← '/posts/[slug]'(詳細)

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 デモ' };

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <header style={{ padding: '12px 24px', background: '#222', color: '#fff' }}>
          <Link href="/" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>ホーム</Link>
          <Link href="/about" style={{ color: '#fff', textDecoration: 'none', marginRight: '16px' }}>紹介</Link>
          <Link href="/posts" style={{ color: '#fff', textDecoration: 'none' }}>ポスト</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>ホーム</h1>
      <p>Next.js で作ったモダン React デモです。</p>
    </div>
  );
}

src/app/posts/page.tsx:

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

const POSTS = [
  { slug: 'hello-world', title: '最初の記事' },
  { slug: 'about-rsc', title: 'RSC とは何か?' },
  { slug: 'tips', title: '学習のコツ' },
];

export default function PostsPage() {
  return (
    <div style={{ padding: '24px' }}>
      <h1>ポスト</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>このページはスラッグ "{slug}" の本文です。</p>
    </div>
  );
}

保存してヘッダーのリンクをクリックして移動してみてください。画面遷移がちらつきなく滑らかに起き、URL も正しく更新されます。

練習問題 #

  1. 上のミニサイトに /posts/[slug]/comments のようなネストされた動的ルートを追加してみてください。フォルダ構造は app/posts/[slug]/comments/page.tsxparams の型は Promise<{ slug: string }> のままです(子パスでも親の動的セグメントがそのまま来ます)。
  2. /docs/(marketing)/landing のような route group を作ってみてください。app/docs/(marketing)/landing/page.tsx。括弧で囲んだフォルダは URL に現れませんが、layout グルーピングに使われます。実際の URL は /docs/landing になります。
  3. not-found.tsx を root ではなく app/posts/ の中に置いて、/posts/missing-slug でアクセスしてみてください。(slug ページで notFound() を呼ぶと、最も近い not-found.tsx がレンダリングされます。)補足: import { notFound } from 'next/navigation'; if (!post) notFound(); パターン。

一行まとめ: Next.js 15 + App Router がこの第4部の環境。src/app/ のフォルダ構造がそのままルーティングであり、page.tsx は画面、layout.tsx は共有の殻。動的ルートは [param] フォルダ、params は Promise。<Link> でクライアントサイド遷移 + prefetch。第15章の React Router がコードで定義していたルートを、ファイルシステムが自動化した形。

次の章 #

これまで作ったページはすべて Server Components でした。ですがクリックイベントや useState のようなインタラクションが入るとどうなるでしょうか。次の 第24章 Server Components vs Client Components では、2 種類のコンポーネントの違いを明確にし、'use client' ディレクティブの役割、そして両方をどう混ぜて使うかを学びます。

X