メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

Next.jsのApp Routerを徹底解説:フォルダ構成でルートと共通レイアウトを決める仕組み

2025年10月15日
最終更新: 2026年10月20日
6分で読めます

この記事でわかること

Next.jsのApp Routerを、ルートとレイアウト、Server・Client Components、データ取得、状態管理、画面遷移の例で解説します。未定義の応答や不正なデータを避ける実装、空一覧とエラーの区別、初回HTMLと操作の関係を整理。小さなページから移行し、何を検証して対象を広げるかを確認できます。

2026年10月8日にNext.js App RouterのServer and Client Components、Layouts and Pages、Fetching Data公式ガイドの必要本文を再確認した。App Routerのlayout/page構成、Server Componentでのデータ取得、Client境界を、URL設計、キャッシュ、認証、失敗表示、ローディング、更新、実装テストへ分ける。公式教材のコードをそのまま自社の認証・秘密情報・性能・データ整合性の保証とせず、合成例・図解・ローカル実行と本番の受入を別に確認する。

App Routerへの移行で最初に決めるのは、フォルダー名だけではありません。データを読む場所、操作を受ける場所、読み込み中や失敗時に見せるものを分ける必要があります。この記事では小さな記事一覧を例に、4つの教材でその境界を確認します。性能向上や移行工数の削減を一律には保証しません。

30秒で要点

  • appのpageはページ、layoutは共通の囲みを担当します。既存のPages Routerを直ちに全部置き換える必要はありません。
  • サーバーでデータを取り、状態やクリック操作が必要な小さい部分をClient Componentにします。
  • 成功データだけでなく、HTTP失敗・形式不正・空一覧を確認します。教材内の取得先は仮想URLで、外部サービスへは送信しません。
App Routerのルート、サーバー取得、Clientの操作、成功と失敗の確認を分けた図
著者作成。ServerとClientの役割分担を示す教材であり、実サイトの性能や移行完了を示しません。

ルートと共通部分を分ける

レイアウトとページの公式説明では、フォルダーがURLの区切りを作り、page.tsxがそのページを定義します。ルートレイアウトはhtmlとbodyを含めます。

表が収まらない場合は、横にスクロールできます

ファイル例役割確認する内容
app/layout.tsx全体の囲みhtml/body、言語、共通ナビゲーション
app/posts/page.tsx/postsのページデータの取得と一覧表示
app/posts/loading.tsx読み込み中の表示待っている内容が分かる文言
app/posts/error.tsx対応する失敗の境界再試行と案内。Client Componentとして実装
app/posts/Counter.tsx操作を受ける部分状態、イベント、キーボード操作

URL構造を変える移行なら、従来URL、リダイレクト、メタデータ、フォーム遷移を一覧にします。同じ見た目でも、検索から来た人や保存済みリンクが同じ場所に到達するとは限りません。

1. Pagesの取得処理をServer Componentへ移す

Pages RouterのgetServerSidePropsで受け渡していたデータを、App Routerでは非同期のページから扱う例です。旧例にあった未定義のresを参照せず、取得した応答をローカル変数に保持します。

app/posts/page.tsxとして独立した教材です。.invalidドメインは接続先ではなく、実装時に自分が管理するAPIへ置き換える印です。データ取得の公式説明を踏まえ、ここでは毎回の取得意図をno-storeで明示します。

type Post = { id: number; title: string };

function parsePosts(value: unknown): Post[] {
  if (!Array.isArray(value)) throw new Error("posts must be an array");
  return value.map((row: unknown) => {
    if (typeof row !== "object" || row === null ||
        !("id" in row) || !Number.isInteger(row.id) ||
        typeof row.id !== "number" || row.id <= 0 ||
        !("title" in row) || typeof row.title !== "string") {
      throw new Error("invalid post");
    }
    return { id: row.id, title: row.title };
  });
}

export default async function Page() {
  const res = await fetch("https://example.invalid/posts", { cache: "no-store" });
  if (!res.ok) throw new Error("posts request failed");
  const posts = parsePosts(await res.json());
  return posts.length ? (
    <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>
  ) : <p>公開記事はまだありません。</p>;
}

parsePostsはこのページ内の関数です。複数のページで使う場合は、lib/posts-data.tsなどの通常モジュールへ移して共有できます。IDの重複など、API固有の整合条件も実装時に加えます。

HTTP200でも返ってきた値が想定どおりとは限りません。失敗を空一覧に変えると「記事がない」と「取得できない」の区別が消えるため、この例では例外を投げ、エラー境界で案内する設計にします。ログへ応答本文や秘密情報をそのまま出す処理も加えていません。

2. サーバーで完結する表示を残す

次はデータを受け取って見出しと本文を表示するServer Componentの例です。クリックや状態が不要な表示までClient Componentへ移す必要はありません。Reactは通常の文字列をテキストとして扱います。

type Props = { title: string; text: string };

export default function ArticleSummary({ title, text }: Props) {
  return <section aria-label="記事の概要"><h2>{title}</h2><p>{text}</p></section>;
}

サーバーだけで扱う資格情報やDBの処理を、Client Componentが読み込むモジュールへ混ぜないようにします。一方、サーバーで動くから認可が不要になるわけではありません。返すデータを利用者が閲覧できるかは、取得処理の前後で明示します。

3. 状態のある部分だけClient Componentにする

Counter.tsxは単独のファイルです。use clientを先頭へ置き、押すたびに増える状態を持たせます。フォーム内へ配置しても意図せず送信しないよう、ボタンの種類を指定します。

"use client";
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p aria-live="polite">回数: {count}</p>
      <button type="button" onClick={() => setCount(value => value + 1)}>増やす</button>
    </div>
  );
}

Server/Client Componentsの資料が説明するuse clientは、モジュール間の境界です。「この部分は初回HTMLに入らず、必ずブラウザーだけで描画する」という意味ではありません。初回表示ではHTMLが作られ、その後イベントなどが結び付けられます。サーバーから渡すpropsも、境界を越えられる値に限定します。

4. 操作による移動を実装する

通常のリンクならリンク要素を使います。操作結果を受けて移動するなど、命令的な遷移が必要な場合の例がuseRouterです。App Routerではnext/navigationから読み込みます。

"use client";
import { useRouter } from "next/navigation";

export default function DashboardButton() {
  const router = useRouter();
  return <button type="button" onClick={() => router.push("/dashboard")}>管理画面へ</button>;
}

この例は固定の自サイト内パスだけを使います。未検証の入力を遷移先へ入れたり、ボタンがあることをアクセス権の証明にしたりしないでください。/dashboard側での認証・認可と、移動後の見出しやフォーカスの分かりやすさは別に確認します。

移行を小さく確認する手順

まず閲覧専用の1ページを選び、旧URL・入力・表示・取得条件を記録します。Server Componentへ移す処理と操作部分を分け、次の確認を通してから対象を増やします。

表が収まらない場合は、横にスクロールできます

場面入力・操作期待する結果
一覧取得正常な2件タイトル2件を表示
空のデータ空配列空一覧の案内
通信の失敗HTTP500相当の応答成功扱いせずエラー経路へ
形式の失敗idが文字列、配列以外parsePostsが拒否
カウンターボタンを2回押す0から2へ変化
管理画面へ移動ボタンを押す固定パスへ1回の遷移要求

教材では代替fetchとローカルDOMで処理を確認します。実際のストリーミング、キャッシュ、画面遷移、支援技術での読み上げ、デプロイ後の速度は、その結果だけでは確認できません。Next.jsのApp Router資料を入口に、対象プロジェクトの版で必要なページを確認しながら進めてください。

よくある質問(FAQ)