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 プロジェクトの作成 #
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 ベースです。
作成が終わったらフォルダに入り、dev サーバーを立ち上げます。
cd modern-react-demo
pnpm devhttp://localhost:3000 にアクセスすると、Next.js のデフォルト画面が見えます。
プロジェクト構造を見てみる #
初めて見る方には馴染みのある部分と見慣れない部分が混在しているはずです。要点だけ整理すると以下のとおりです。
modern-react-demo/
├── public/ ← 静的ファイル(画像など)
├── src/
│ └── app/ ← ここが核心。ルーティングが始まる場所
│ ├── layout.tsx ← すべてのページの共通レイアウト
│ ├── page.tsx ← '/' パスのページ
│ ├── globals.css ← グローバルスタイル
│ └── favicon.ico
├── package.json
├── next.config.ts
└── tsconfig.jsonVite プロジェクトとの最大の違いは、src/app/ フォルダのファイルとフォルダ構造自体がルーティングになる という点です。URL パスの 1 つひとつがフォルダで、その中の page.tsx が画面を描画します。これが ファイルベースルーティング です。
第15章(React Router)の <Route path="/about" element={<About />} /> モデルと比較すると、ルート定義をコードではなく ディレクトリ構造 が担うのが決定的な違いです。
最も単純なページ #
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:
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:
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 を使ってください。
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(すでに自動生成されています):
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 がネストされるのです。
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:
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.tsx、layout.tsx 以外にも、フォルダに置くと自動的に動作する特別なファイルがあります。
| ファイル | 役割 | 本書で扱う位置 |
|---|---|---|
page.tsx | ルートの画面(必須) | 本章 |
layout.tsx | そのフォルダ以下の共通レイアウト | 本章 |
loading.tsx | Suspense fallback | 第26章 |
error.tsx | エラー境界 | 第33章(Sentry)と組 |
not-found.tsx | 404 画面 | 本章(オプション) |
route.ts | API ルート(ページではなくエンドポイント) | 第27章で一部 |
template.tsx | layout と似ているが毎回再マウントされる版 | (本書範囲外) |
(group) | route group(URL に出ないグルーピングフォルダ) | 第34章キャップストーン |
今すぐ覚える必要はなく、「こういうものがある」程度に知っておけばよいです。この第4部を進める中で順番に登場します。
not-found.tsx — 404 #
マッチするルートがないときの画面は 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.tsx | app/page.tsx |
| 動的ルート | pages/posts/[slug].tsx | app/posts/[slug]/page.tsx |
| レイアウト | _app.tsx 単一 / 直接合成 | app/layout.tsx 自動ネスト |
| データフェッチ | getServerSideProps / getStaticProps | Server Component 関数本体(第25章) |
| API ルート | pages/api/*.ts | app/.../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:
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:
export default function HomePage() {
return (
<div style={{ padding: '24px' }}>
<h1>ホーム</h1>
<p>Next.js で作ったモダン React デモです。</p>
</div>
);
}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:
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 も正しく更新されます。
練習問題 #
- 上のミニサイトに
/posts/[slug]/commentsのようなネストされた動的ルートを追加してみてください。フォルダ構造はapp/posts/[slug]/comments/page.tsx。paramsの型はPromise<{ slug: string }>のままです(子パスでも親の動的セグメントがそのまま来ます)。 /docs/(marketing)/landingのような route group を作ってみてください。app/docs/(marketing)/landing/page.tsx。括弧で囲んだフォルダは URL に現れませんが、layout グルーピングに使われます。実際の URL は/docs/landingになります。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' ディレクティブの役割、そして両方をどう混ぜて使うかを学びます。