🚀 はじめに:この記事でできること

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:セグメント単位のローディングUI
    • app/**/error.tsx:セグメント単位のエラーUI
    • app/api/**/route.ts:HTTPメソッドごとのAPIハンドラ

目的

  • ファイル規約とRSCの前提を結び付けて理解する

結果(この時点でできること)

  • 「どのファイルが何を担うか」を説明できる

補足
レイアウトはナビゲーションでも状態が保持されやすい設計(ページだけ差し替え)です。


🗺️ 図解:App Routerのディレクトリと流れ

最小構成の関係性を図で理解します。

操作

  • 以下の図/ツリーで、URLとファイルの対応を確認する

目的

  • セグメント(フォルダ)=URLという対応付けを視覚的に理解する

結果(この時点でできること)

  • どの階層にpage.tsx等を置けばURLが有効化されるか判断できる
Next.js App Routerの構造

補足
layout.tsxは上位レイアウトを継承しながら子ページを差し替えます。


🧭 Pages Router vs App Router(初心者向け比較)

違いを要点で押さえます。

操作

  • 次の比較表で、移行や新規の判断材料を確認する

目的

  • Pagesの慣れから来る誤解を早期に解消する

結果(この時点でできること)

  • 「AppではPages専用APIを使わない」などの原則を理解
観点Pages Router(/pages)App Router(/app)
ディレクトリpages/**がページ化。_app.tsx / _document.tsxで全体制御フォルダ=URLpage.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をセグメントに配置
  • メタ情報不足metadatatitle/description/og/twitterを整備
  • 用語ゆれ/表記ゆれ:本文とFront Matterの語彙(例:RSC、Server Actions)を統一

📚 参考リンク(公式優先)


🔧 拡張案(次にやると良いこと)

  1. メタデータとOGPの整備export const metadata をレイアウトで定義し、Open Graph/Twitter を継承設定。
  2. Route Handlersで簡易APIapp/api/**/route.ts にGET/POSTなどを配置し、UIと同居させる。
  3. キャッシュの設計force-cache / no-store / next.revalidaterevalidateTag/Path を場面で使い分け。
  4. Server Actionsの実務化:バリデーション・楽観的UI・権限チェックを追加し、DXを高める。

🎯 まとめ

  • App Router=Server Firstapp/のファイル規約でレイアウト・データ取得・APIを一体的に扱える
  • Pages専用APIは使用しない。RSC+ルート設定とServer Actions/Route Handlersを基本に
  • キャッシュと再検証の理解が、速さと鮮度の両立と安定運用の鍵