Contents
App Router と Pages Router の概要
Next.js(13.x 以降)では、App Router がデフォルトのルーティング方式として提供されており、従来の Pages Router とは設計思想が大きく異なります。本セクションでは、両者の基本的な違いと、実務でどちらを選択すべきかの指針を解説します。
App Router はサーバーコンポーネントを中心に据えた 「UI とデータ取得をディレクトリ構造だけで完結させる」 アプローチです。一方、Pages Router はページ単位でクライアント側のロジックを記述し、getStaticProps などの従来のデータ取得手法に依存します。以下では具体的な相違点を見ていきます。
ルーティングモデルの違い
App Router は app/ ディレクトリ配下に配置した page.tsx / layout.tsx が自動的に URL にマッピングされます。Pages Router は pages/ 配下のファイル名がそのままパスになる従来型です。
| 項目 | App Router (app/) |
Pages Router (pages/) |
|---|---|---|
| ルート定義方式 | ファイルシステムベース(ページ・レイアウトはディレクトリ階層で管理) | 同様にファイルシステムベースだが、_app.js / _document.js が全体レイアウトを担う |
| デフォルトコンポーネント種別 | Server Component(サーバー側で実行) | Client Component(ブラウザで実行) |
| データ取得の主流手法 | fetch / use() で直接サーバーサイドに記述 |
getStaticProps, getServerSideProps などの API を使用 |
コンポーネントのデフォルト挙動
App Router では Server Component が標準 とされ、クライアント側でだけ必要なロジックがある場合にのみ "use client" ディレクティブを付与します。Pages Router のページは基本的にクライアントコンポーネントとして扱われるため、サーバーサイド専用の API(例:fetch)は getStaticProps など経由で呼び出す必要があります。
データ取得と ISR の統合
App Router ではページやレイアウト内部で 非同期関数をそのまま使用 でき、next: { revalidate } オプションで Incremental Static Regeneration (ISR) を簡単に設定できます。Pages Router でも ISR は可能ですが、getStaticProps の中で revalidate を返す形になるため、記述がやや冗長になります。
プロジェクトの作成とディレクトリ構成
このセクションでは、最新の Next.js(13 以降)で App Router を利用したプロジェクトをゼロから作成する手順と、実務で役立つディレクトリ構成例を紹介します。
初期化手順
以下のコマンドで TypeScript 環境が有効な Next.js アプリケーションを生成し、そのまま App Router 用に整備します。create-next-app@latest が常に最新版を取得するポイントです。
|
1 2 3 4 5 6 |
# プロジェクト作成(TypeScript 対応) npx create-next-app@latest my-next-app --ts # 作業ディレクトリへ移動 cd my-next-app |
生成されたプロジェクトはデフォルトで pages/ ディレクトリが含まれます。App Router を使用するためにこのフォルダを削除し、代わりに app/ ディレクトリを作成します。
|
1 2 3 4 5 6 |
# Pages Router 用のディレクトリを削除 rm -rf pages # App Router 用ディレクトリを作成 mkdir app |
推奨される app/ ディレクトリ構造
App Router では レイアウトは階層ごとに上書き可能 な点が大きな特徴です。以下は実務でよく採用される例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
app/ ├─ layout.tsx # ルート全体の共通レイアウト(ヘッダー・フッター) ├─ page.tsx # / → ホームページ │ ├─ dashboard/ # ダッシュボード領域 │ ├─ layout.tsx # Dashboard 用ネストレイアウト │ └─ page.tsx # /dashboard のエントリーページ │ └─ products/ ├─ [id]/ # 動的ルーティング(/products/:id) │ └─ page.tsx └─ page.tsx # /products の一覧ページ |
layout.tsxはそのディレクトリ以下すべてに適用され、子ディレクトリで再定義すると上書きされます。page.tsxが実際の UI を担い、サーバーコンポーネントとしてデータ取得が可能です。
ファイルベースルーティングとレイアウト実装
この章では、具体的なコード例を通して App Router のページ作成・レイアウト定義・ネストレイアウトの書き方を学びます。サーバーコンポーネントがデフォルトであることに注意しながら、必要に応じてクライアント側ロジックを分離します。
基本的なページ実装例 (app/page.tsx)
トップページは app/page.tsx に配置します。サーバーコンポーネントなので、fetch を直接使用して外部 API からデータを取得し、そのまま描画できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// app/page.tsx import Image from "next/image"; export const metadata = { title: "ホーム | My Next.js アプリ", }; export default async function HomePage() { // ISR 用に 30 秒ごとに再生成 const res = await fetch("https://api.example.com/hero", { next: { revalidate: 30 }, }); const data = await res.json(); return ( <section> <h1>{data.title}</h1> <Image src={data.image} alt="Hero" width={1200} height={600} /> <p>{data.description}</p> </section> ); } |
ポイントは next: { revalidate } オプションで ISR を有効にしている点です。これによりビルド時だけでなく、リクエストごとに最新データが取得されます。
共有レイアウト (app/layout.tsx)
全ページで共通のヘッダー・フッターを提供する layout.tsx はサーバーコンポーネントとして実装します。metadata オブジェクトは自動的に <head> に注入されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// app/layout.tsx import "./globals.css"; export const metadata = { title: "My Next.js アプリ", description: "App Router を活用したサンプルプロジェクト", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ja"> <body> <header className="site-header">🚀 My Site</header> {children} <footer className="site-footer">©2026 Example Corp.</footer> </body> </html> ); } |
ネストレイアウトで UI を分離する (app/dashboard/layout.tsx)
ダッシュボードのようにページごとに異なるサイドバーやナビゲーションが必要な場合は、子ディレクトリに layout.tsx を配置 します。親のレイアウトを継承しつつ、追加要素だけを書き加える形になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// app/dashboard/layout.tsx export const metadata = { title: "Dashboard | My Next.js アプリ", }; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="dashboard-wrapper"> <aside className="sidebar">← メニュー</aside> <main>{children}</main> </div> ); } |
この layout.tsx が適用されるのは /dashboard/** 以下すべてです。ページ (app/dashboard/page.tsx) は自動的に上記レイアウトでラップされます。
コンポーネントの種類とデータ取得戦略
App Router では Server Component と Client Component の境界を明示的に管理 することがパフォーマンス最適化の鍵です。また、use() フックや ISR オプションの正しい使い方も重要です。
Server Component と Client Component の判定基準
| 判定項目 | Server Component(デフォルト) | Client Component |
|---|---|---|
| 実行環境 | Node.js(サーバー側) | ブラウザ |
| データ取得 | fetch / use() が直接使用可 |
useEffect などクライアント側 API 必要 |
| React フック | 不可(useState, useEffect なし) |
完全に利用可能 |
| 宣言方法 | ファイル冒頭に何も書かない | "use client" を先頭行に記述 |
実務上の指針:UI がデータ取得だけで完結する場合は Server Component に留め、インタラクティブなロジック(クリックハンドラ等)が必要な箇所だけ
"use client"を付与します。
fetch と ISR の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// app/products/page.tsx (Server Component) export const metadata = { title: "商品一覧", }; export default async function ProductsPage() { const res = await fetch("https://api.example.com/products", { // 5 分ごとに再生成(ISR) next: { revalidate: 300 }, }); const products = await res.json(); return ( <ul> {products.map((p: { id: string; name: string }) => ( <li key={p.id}>{p.name}</li> ))} </ul> ); } |
next: { revalidate } が公式に推奨される ISR 指定方法です。旧来の cache: "force-cache" は非推奨となりつつあるため、可能な限り上記形で統一してください。
use() フックの注意点
Next.js 13.4 以降、React の use(実験的)をサーバーコンポーネント内で利用できますが、以下の制約があります。
- Server Component 限定:クライアント側では使用できません。
- 非同期関数は必ずキャッシュ可能:内部で
fetchを行う場合はnext: { revalidate }等のオプションを付与し、キャッシュ戦略を明示してください。 - 安定性は未保証:現在は「experimental」フラグ付き機能であり、将来的な API 変更があり得ます。プロダクション環境では必ずテストを行い、必要に応じて
awaitを用いた従来の書き方にフォールバックできるようにしておくと安全です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// app/dashboard/page.tsx(Server Component) import { use } from "react"; async function getStats() { const res = await fetch("https://api.example.com/stats", { next: { revalidate: 60 }, }); return res.json(); } export default function DashboardPage() { // `use` により非同期データ取得を宣言的に記述 const stats = use(getStats()); return ( <section> <h2>統計情報</h2> <p>利用者数:{stats.users}</p> <p>売上合計:¥{stats.sales}</p> </section> ); } |
Suspense と lazy の組み合わせ(クライアントコンポーネント)
インタラクティブな UI 部分で 遅延ロード が必要な場合は、React.lazy と Suspense を併用します。以下はチャート描画を遅延させる例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// app/components/ChartClient.tsx "use client"; import { Suspense, lazy } from "react"; const HeavyChart = lazy(() => import("./HeavyChart")); export default function ChartClient() { return ( <Suspense fallback={<p>Loading chart...</p>}> <HeavyChart /> </Suspense> ); } |
"use client" が付与されたファイルだけがクライアント側にバンドルされ、サーバーコンポーネントのサイズ増大を防げます。
動的ルート・ミドルウェア・Route Handlers(API Routes)
App Router でも 動的セグメント や Edge Middleware、そして従来の API Routes に相当する Route Handlers が利用可能です。ここでは実装例と注意点を示します。
動的ページ (app/products/[id]/page.tsx)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// app/products/[id]/page.tsx(Server Component) import { notFound } from "next/navigation"; export const generateMetadata = async ({ params }: { params: { id: string } }) => ({ title: `商品 ${params.id} の詳細`, }); export default async function ProductDetail({ params, }: { params: { id: string }; }) { const res = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 120 }, }); if (!res.ok) notFound(); const product = await res.json(); return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> <p>価格:¥{product.price}</p> </article> ); } |
params.idが URL の動的部分に自動マッピングされます。notFound()はサーバー側で 404 ページを表示させるユーティリティです。
Edge Middleware(認証チェックの例)
Middleware は Edge Runtime 上で実行でき、リクエストがページや API に到達する前に処理できます。以下はトークンベース認証を行うシンプルな例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// middleware.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export async function middleware(request: NextRequest) { const token = request.cookies.get("authToken")?.value; if (!token || !(await verify(token))) { // 認証失敗 → ログインページへリダイレクト return NextResponse.redirect(new URL("/login", request.url)); } // 認証成功 → 通常の処理に進む return NextResponse.next(); } // JWT の有効期限だけをチェックする簡易実装例 async function verify(token: string): Promise<boolean> { try { const payload = JSON.parse( Buffer.from(token.split(".")[1], "base64").toString() ); return Date.now() < payload.exp * 1000; } catch { return false; } } // /dashboard 以下にのみ適用 export const config = { matcher: "/dashboard/:path*", }; |
matcher を設定することで、不要なリクエストへのミドルウェア実行を防ぎ、パフォーマンス向上が期待できます。
Route Handlers(App Router の API エンドポイント)
pages/api/ に代わる Route Handlers は app/api/ 配下に配置し、HTTP メソッドごとに関数をエクスポートします。以下は商品取得と削除の例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// app/api/products/[id]/route.ts import { NextResponse } from "next/server"; export async function GET( request: Request, { params }: { params: { id: string } } ) { const res = await fetch(`https://external-api.example.com/products/${params.id}`); const data = await res.json(); return NextResponse.json(data); } export async function DELETE( request: Request, { params }: { params: { id: string } } ) { const authHeader = request.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return new Response("Unauthorized", { status: 401 }); } await fetch(`https://external-api.example.com/products/${params.id}`, { method: "DELETE", }); return new Response(null, { status: 204 }); } |
GET/POST/PUT/DELETEといったメソッドを関数名でエクスポートするだけで、Edge Functions として自動デプロイされます。requestオブジェクトは標準の Web API 互換なので、ヘッダーやボディへのアクセスがシンプルです。
SEO・パフォーマンス最適化と Vercel デプロイ
App Router が提供する metadata 機構を活用すれば、SEO 設定がコードベースに統合されます。また、画像最適化やコード分割のテクニックと併せて、Vercel へのデプロイ手順も合わせて解説します。
metadata による SEO 設定
ページ単位で export const metadata = { … } を記述すると、Next.js が自動的に <head> タグへ注入します。動的パラメータが必要な場合は関数形式でも構いません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// app/products/[id]/page.tsx export const generateMetadata = async ({ params, }: { params: { id: string }; }) => ({ title: `商品 ${params.id} の詳細`, description: "高品質な商品情報を提供します。", openGraph: { images: "/og-image.png", }, }); |
generateMetadata はページがリクエストされるたびに実行され、SSR 時点で正しい meta 情報が生成されます。
画像・コード分割によるパフォーマンス改善
| 項目 | 推奨手法 |
|---|---|
| 画像最適化 | next/image の自動サイズ調整と lazy‑loading(デフォルト有効)。外部ホストの場合は loader をカスタマイズ。 |
| 大規模ライブラリ | クライアント側でだけ必要な場合は dynamic import + Suspense で遅延ロード。例:import("./HeavyLib") |
| CSS 管理 | グローバルは globals.css、コンポーネント単位は CSS Modules(Component.module.css)を使用し不要なスタイルのバンドルを防止。 |
| データキャッシュ | ISR (next: { revalidate }) と タグベース再生成(Next.js 13.4+)で細かなキャッシュ制御が可能。 |
Vercel へのデプロイ手順
- Vercel CLI のインストール
bash
npm i -g vercel
- Git リポジトリを作成しプッシュ(GitHub が推奨)
bash
git init
git add .
git commit -m "Initial Next.js App Router project"
git branch -M main
git remote add origin https://github.com/yourname/my-next-app.git
git push -u origin main
- Vercel にプロジェクトを作成
bash
vercel login # アカウントでログイン
vercel # ディレクトリ内で実行、指示に従って設定
- フレームワーク検出は自動的に Next.js と判定されます。
-
ビルドコマンドは
npm run build、デプロイ先はデフォルトのプレビュー環境です。 -
環境変数を設定
Vercel ダッシュボード → 「Settings」→「Environment Variables」で NEXT_PUBLIC_API_KEY 等必要なキーを追加します。Production と Preview の両方に同じ名前で設定すると、ローカルと同様に利用できます。
- デプロイ完了の確認
デプロイが成功するとプレビュー URL が表示されます。その URL を本番ドメイン(例:example.com)へ割り当てれば運用開始です。Vercel は Edge Functions と連携し、先述の Middleware や Route Handlers も自動的に最適化された形で提供します。
まとめ
- App Router vs Pages Router
- App Router は Server Component がデフォルトで、レイアウト・データ取得がディレクトリ構造だけで完結。
-
Pages Router は従来通りクライアントコンポーネント中心で、
getStaticProps系が必要。 -
プロジェクト作成
-
npx create-next-app@latest --ts→app/ディレクトリに切り替えるだけで即座に App Router が有効化できる。 -
ルーティングとレイアウト
-
page.tsxがページ本体、layout.tsxが階層ごとの共通レイアウト、子ディレクトリのlayout.tsxで UI を細分化可能。 -
コンポーネントとデータ取得
-
Server Component と Client Component の境界を意識し、
fetch(..., { next: { revalidate } })で ISR を導入。
–use()は実験的機能なので安定版へのフォールバックも考慮。 -
動的ルート・ミドルウェア・Route Handlers
-
[id]/page.tsxによる動的ページ、Edge Middleware で認証、app/api/…/route.tsが API エンドポイントとして機能。 -
SEO・パフォーマンスとデプロイ
metadata/generateMetadataで SEO をコードベースに統合。next/imageと動的インポートで最適化し、Vercel にシームレスにデプロイできる。
以上のポイントを押さえておけば、Next.js(13 以降)の App Router を実務レベルで安全かつ効率的に活用できます。公式ドキュメントは随時更新されますので、最新情報は https://nextjs.org/docs/app を定期的に確認してください。