🚀 はじめに:この記事でできること
App Routerの基本概念から最小実装、データ取得、Server Actions/Route Handlersまでを「操作→目的→結果→注意/補足」の順で習得します。ゴールは、app/ディレクトリで最小アプリを動かし、RSCのデータ取得と更新を体験できる状態です。
🧭 前提:対応バージョンと用語の整理
App RouterはNext.js 13.4以降で本格化したルーティングです。Pages専用API(getServerSideProps等)はApp Routerでは非推奨で、RSC(React Server Components)とファイル規約が基本になります。
操作
- プロジェクトのNext.jsバージョンを確認し、
app/ディレクトリが利用可能かを確認する - 用語を把握:
layout.tsx(レイアウト)/page.tsx(ルート)/loading.tsx/error.tsx/route.ts(API)/ RSC / Server Actions
目的
- 以降の手順で使う概念・ファイル規約の前提をそろえ、混乱を防ぐ
結果(この時点でできること)
- App Routerの章立てと各ファイル役割を前提知識として共有できる
注意
- Pages RouterのAPI(
getServerSideProps等)はApp Routerでは使わない- クライアントでのみ動くAPI(
window等)はClient Componentに分離("use client"を付与)
🧩 App Routerとは?(概要)
App Routerはフォルダ=URL、page.tsx=公開ルート、layout.tsx=共通UIという規約で、RSC前提の「サーバ優先(Server First)」な構成を提供します。
操作
- 役割を把握:
app/layout.tsx:アプリの共通レイアウトapp/page.tsx:トップページapp/**/loading.tsx:セグメント単位のローディングUIapp/**/error.tsx:セグメント単位のエラーUIapp/api/**/route.ts:HTTPメソッドごとのAPIハンドラ
目的
- ファイル規約とRSCの前提を結び付けて理解する
結果(この時点でできること)
- 「どのファイルが何を担うか」を説明できる
補足
レイアウトはナビゲーションでも状態が保持されやすい設計(ページだけ差し替え)です。
🗺️ 図解:App Routerのディレクトリと流れ
最小構成の関係性を図で理解します。
操作
- 以下の図/ツリーで、URLとファイルの対応を確認する
目的
- セグメント(フォルダ)=URLという対応付けを視覚的に理解する
結果(この時点でできること)
- どの階層に
page.tsx等を置けばURLが有効化されるか判断できる

補足
layout.tsxは上位レイアウトを継承しながら子ページを差し替えます。
🧭 Pages Router vs App Router(初心者向け比較)
違いを要点で押さえます。
操作
- 次の比較表で、移行や新規の判断材料を確認する
目的
- Pagesの慣れから来る誤解を早期に解消する
結果(この時点でできること)
- 「AppではPages専用APIを使わない」などの原則を理解
| 観点 | Pages Router(/pages) | App Router(/app) |
|---|---|---|
| ディレクトリ | pages/**がページ化。_app.tsx / _document.tsxで全体制御 | フォルダ=URL、page.tsx が公開。layout.tsxがルート/ネストで状態保持 |
| レイアウト | 公式一体型でなくパターンで対応 | ネストレイアウトが第一級機能 |
| データ取得 | getServerSideProps/getStaticProps | **RSC内のasync/await**とfetchのキャッシュ/再検証 |
| ローディング/エラー | ページ側で実装 | loading.tsx / error.tsx を配置 |
| API | /pages/api/** | /app/api/**/route.ts にHTTPメソッド関数 |
| ミューテーション | フォーム→API→更新が基本 | Server Actions("use server")でフォームから直接サーバ関数 |
注意
getServerSidePropsなどPages専用APIはAppでは非推奨。同様のニーズはRSC+ルート設定(dynamic = "force-dynamic"等)で置き換えます。
🏁 Step 1:プロジェクト作成と起動
新規プロジェクトを作成し、開発サーバーを起動します。
操作
npx create-next-app@latest my-app
cd my-app
npm run dev
目的
- App Routerが有効な初期プロジェクトを用意し、ローカルで確認できる状態にする
結果(この時点でできること)
http://localhost:3000/で初期画面が表示される
注意
Nodeのバージョン要件・パッケージマネージャーはプロンプトに従って選択。既存プロジェクトに導入する場合はapp/の構成と競合に注意。
🧱 Step 2:基本ページ(app/page.tsx)を作る
RSC(サーバコンポーネント)で最小ページを実装します。
操作
目的:RSC既定のレンダリングを最小例で体感する
前提:"use client"を付けない=Server Component
// app/page.tsx — Server Component("use client" は付けない)
export default async function Page() {
const now = new Date().toISOString();
return (
<main style={{ padding: 24 }}>
<h1>App Router はじめの一歩 🎉</h1>
<p>サーバで描画({now})</p>
</main>
);
}
結果(この時点でできること)
- トップページがサーバ側で描画され、時刻表示が更新される
注意
ブラウザAPI(window等)が必要な処理はClient Componentに分離し、コンポーネント先頭に"use client"を付けます。
📦 Step 3:レイアウト(app/layout.tsx)を定義
共通レイアウトを定義して、ページ間で状態を保ちます。
操作
目的:共通UIと状態保持、メタデータ継承の基盤を作る
前提:レイアウトは上位から下位に継承される
// app/layout.tsx — ルートレイアウト(必須)
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
<header>My App</header>
{children}
<footer>© My App</footer>
</body>
</html>
);
}
結果(この時点でできること)
- ナビゲーション時もレイアウトが再マウントされにくく、状態が保持されやすい
補足
export const metadata = { ... }をレイアウトやページに加えると、OG/Twitterカードなどのメタ情報を構造化できます。
📡 Step 4:データ取得(RSCとキャッシュ/再検証)
Server Component内でfetchを使い、キャッシュや再検証を理解します。
操作
目的:RSC内でのデータ取得とcache/revalidateの使い分けを学ぶ
前提:必要に応じてforce-cache/no-store/next.revalidateを指定
// app/blog/page.tsx — 一覧をRSCで取得
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog', {
// 例: { cache: 'no-store' } または { next: { revalidate: 60 } }
});
const posts = await res.json();
return (
<main>
<h1>ブログ一覧</h1>
<ul>
{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}
</ul>
</main>
);
}
結果(この時点でできること)
- ページごとに鮮度と速度のトレードオフを調整できる
補足
イベント駆動の再検証にはrevalidatePath/revalidateTagを使用(タグ付けして無効化範囲を制御)。
⏳ Step 5:ローディング/エラーUI(loading.tsx/error.tsx)
セグメント単位でローディングとエラー境界を用意します。
操作
目的:ユーザー体験を損なわずにデータ待ち・失敗時を表現
前提:error.tsxはクライアント境界(内部で"use client"可能)
// app/blog/loading.tsx — ローディングUI
export default function Loading() {
return <p>読み込み中...</p>;
}
// app/blog/error.tsx — エラーUI(必要に応じて "use client")
'use client';
export default function Error({ error }: { error: Error }) {
return <p>エラーが発生しました:{error.message}</p>;
}
結果(この時点でできること)
- データ取得中やエラー時にも途切れないUXを実現
注意
error.tsxはセグメント境界ごとに置く点に注意。ログ送出など副作用はクライアント側で扱います。
🛣️ Step 6:Route Handlers(/app/api/**/route.ts)
App Router配下にAPIを同居させ、UIと近接したデータ提供を行います。
操作
目的:UIとAPIを同居させ、関心の近いコードを保守しやすくする
前提:HTTPメソッド(GET/POST等)ごとに関数をエクスポート
// app/api/ping/route.ts — 最小のRoute Handler
export async function GET() {
return new Response(JSON.stringify({ pong: true }), {
headers: { 'content-type': 'application/json' },
});
}
結果(この時点でできること)
GET /api/pingでJSONレスポンスを返せる
補足
認証やCORSが必要な場合はミドルウェアやヘッダーで調整します。
🧪 Step 7:Server Actions(フォーム送信→更新→再描画)
フォームから直接サーバ関数を呼び出し、UIを更新します。
操作
目的:APIルートを介さず最短経路でミューテーションを体験
前提:Server Actionはサーバのみで実行。引数・戻り値はシリアライズ可能であること
// app/actions.ts — Server Actions(全エクスポートはサーバ実行)
'use server';
let items: string[] = [];
export async function addItem(_state: { error?: string }, formData: FormData) {
const title = (formData.get('title') as string)?.trim();
if (!title) return { error: 'タイトルを入力してください' };
items.unshift(title);
return { error: '' };
}
export async function listItems() {
return items;
}
// app/page.tsx — 一覧表示とフォーム(RSC & Client)
import { listItems, addItem } from './actions';
import { revalidatePath } from 'next/cache';
export default async function Page() {
const data = await listItems();
async function addAndRevalidate(state: any, fd: FormData) {
'use server';
const res = await addItem(state, fd);
revalidatePath('/');
return res;
}
return (
<main style={{ padding: 24 }}>
<h1>Server Actions で追加</h1>
<Form items={data} action={addAndRevalidate} />
</main>
);
}
// Client側コンポーネント
'use client';
import { useActionState } from 'react';
function Form({ items, action }: { items: string[]; action: any }) {
const [state, formAction, pending] = useActionState(action, { error: '' });
return (
<>
<form action={formAction} style={{ display: 'flex', gap: 8 }}>
<input name="title" placeholder="アイテム" />
<button disabled={pending}>追加</button>
</form>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
<ul>
{items.map((t, i) => <li key={i}>{t}</li>)}
</ul>
</>
);
}
結果(この時点でできること)
- 送信でリストが更新され、
revalidatePath('/')によりUIが即時反映される
注意
Server Actionsはサーバ実行に限定。ファイル境界・ビルド環境の差により動かない場合は"use server"宣言やエクスポート位置を再確認。
✅ よくあるつまずき(チェックリスト)
- Pages APIをAppで使おうとしている:
getServerSideProps等はPages専用。AppではRSC/ルート設定で代替 - レイアウトが毎回リセット:
layout.tsxの配置とネストを確認(状態保持はレイアウト単位) - データが更新されない/古い:
cache: 'no-store'/next.revalidate/revalidateTag/Pathの使い分け - ブラウザAPIをRSCで呼ぶ:
"use client"のClient Componentへ分離 - API設計が冗長:フォーム中心の更新はまずServer Actionsを検討
- Route Handlersの応答が不正:
content-typeやレスポンス形式を明示 - 階層とURLが不一致:セグメント(フォルダ)=URL。
page.tsxが公開ポイント - ローディング/エラー未整備:
loading.tsx/error.tsxをセグメントに配置 - メタ情報不足:
metadataでtitle/description/og/twitterを整備 - 用語ゆれ/表記ゆれ:本文とFront Matterの語彙(例:RSC、Server Actions)を統一
📚 参考リンク(公式優先)
- App Routerのレイアウト/ページ構成:https://nextjs.org/docs/app/building-your-application/routing
- Route Handlers(
route.ts):https://nextjs.org/docs/app/building-your-application/routing/route-handlers - データ取得(RSC/Client):https://nextjs.org/docs/app/building-your-application/data-fetching/fetching
- キャッシュ/再検証:https://nextjs.org/docs/app/building-your-application/caching
revalidateTagAPI:https://nextjs.org/docs/app/api-reference/functions/revalidateTag- Pages Router(比較用):https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts
🔧 拡張案(次にやると良いこと)
- メタデータとOGPの整備:
export const metadataをレイアウトで定義し、Open Graph/Twitter を継承設定。 - Route Handlersで簡易API:
app/api/**/route.tsにGET/POSTなどを配置し、UIと同居させる。 - キャッシュの設計:
force-cache/no-store/next.revalidateとrevalidateTag/Pathを場面で使い分け。 - Server Actionsの実務化:バリデーション・楽観的UI・権限チェックを追加し、DXを高める。
🎯 まとめ
- App Router=Server First。
app/のファイル規約でレイアウト・データ取得・APIを一体的に扱える - Pages専用APIは使用しない。RSC+ルート設定とServer Actions/Route Handlersを基本に
- キャッシュと再検証の理解が、速さと鮮度の両立と安定運用の鍵
