Next.jsのApp Routerを導入したはいいものの、「このコンポーネント、どこに置くのが正解なんだろう?」「大規模になったときに破綻しないフォルダ構造ってどうすればいいんだろう?」と悩んでいませんか? 特に、Server ComponentsとClient Componentsが混在する中で、どこに境界線を引くべきか迷う方も多いでしょう。
この記事では、Next.js App Routerを用いた大規模アプリケーション開発において、保守性とスケーラビリティを最大化する「Colocation(併置)」の考え方に基づいた効果的なフォルダ設計術を、具体的なコード例と共に解説します。この記事を読めば、あなたのプロジェクトは明確な構造を持ち、開発効率とメンテナンス性が飛躍的に向上するはずです。
Next.js App Routerにおけるフォルダ設計の課題とColocationの重要性
Next.js App Routerは、React Server Componentsやネストされたレイアウトといった強力な機能を提供し、アプリケーション開発のパラダイムを大きく変えました。しかし、その自由度の高さゆえに、適切なフォルダ設計の指針がないと、コードベースがすぐに複雑化し、チーム開発における認知負荷を高めてしまいます。
特に、App Routerが推奨する「Colocation(併置)」という考え方は、コンポーネント、スタイル、ヘルパー関数などを、それらが属するルートの近くに配置することで、関連性の高いファイルを一箇所に集約し、見通しを良くすることを目的としています。このセクションでは、なぜColocationが重要なのか、その基本原則を解説します。
App Routerの基本とファイルシステムベースルーティング
Next.js App Router(バージョン13.4以降、Next.js 14安定版を含む)は、appディレクトリ内のフォルダ構造がそのままURLパスに対応するファイルシステムベースのルーティングを採用しています。例えば、app/dashboard/page.tsxは/dashboardというURLにマッピングされます。
この特性を活かし、ルーティングに関わるファイル(page.tsx, layout.tsxなど)だけでなく、そのルート固有のコンポーネントやロジックも同じフォルダ階層に配置するのがColocationの基本です。
Colocationのメリット
- 認知負荷の軽減: 特定の機能やページに関連するすべてのファイルが近くにあるため、コードを追いかける手間が省けます。
- 保守性の向上: 機能の変更や削除の際、影響範囲が限定的になり、関連ファイルを一度に見つけやすくなります。
- スケーラビリティ: アプリケーションが大規模になっても、各機能が独立したモジュールとして管理しやすくなります。
- 新機能開発の効率化: 新しい機能を追加する際に、既存の構造を参考にしやすくなります。
Colocationを最大化するNext.js App Routerのフォルダ設計パターン
ここからは、Colocationの考え方に基づいた具体的なNext.js App Routerのフォルダ設計パターンを解説します。大規模アプリケーションでの利用を想定し、共有コンポーネント、APIルート、ユーティリティなどの配置についても言及します。
プロジェクトの初期設定
まず、App Routerを使用するNext.jsプロジェクトを作成します。
npx create-next-app@latest my-app --typescript --eslint --app
--app フラグにより、App Routerが有効になります。
基本的なフォルダ構造の全体像
以下のフォルダ構造は、Colocationの原則とNext.jsの推奨事項を組み合わせたものです。
my-nextjs-project/
├── app/
│ ├── (main)/ # ルートグループ: メインコンテンツのルートを整理
│ │ ├── dashboard/
│ │ │ ├── page.tsx # /dashboard に対応するページ (Server Component)
│ │ │ ├── layout.tsx # /dashboard およびその子ルートに適用されるレイアウト (Server Component)
│ │ │ └── _components/ # dashboard 固有のコンポーネント (プライベートフォルダ)
│ │ │ ├── DashboardHeader.tsx
│ │ │ └── ChartDisplay.tsx
│ │ ├── auth/
│ │ │ ├── login/
│ │ │ │ ├── page.tsx # /auth/login に対応するページ
│ │ │ │ └── _components/ # login 固有のコンポーネント
│ │ │ │ └── LoginForm.tsx ("use client" を含む Client Component)
│ │ │ └── _components/ # auth セグメント内で共有されるコンポーネント
│ │ │ └── GitHubSignInButton.tsx
│ ├── api/
│ │ ├── users/
│ │ │ └── route.ts # /api/users に対応する Route Handler (GET, POSTなど)
│ ├── layout.tsx # ルートレイアウト (<html>, <body> タグを含む、Server Component)
│ └── page.tsx # ホームページ (/) (Server Component)
├── components/ # アプリケーション全体で再利用されるUIコンポーネント
│ ├── ui/ # プリミティブなUIコンポーネント (Button, Inputなど)
│ │ ├── Button.tsx
│ │ └── Modal.tsx
│ └── common/ # アプリケーション全体で共通のレイアウトコンポーネント (Header, Footerなど)
│ └── Header.tsx
├── public/ # 静的ファイル (画像、アイコンなど)
├── styles/ # グローバルCSSや共有スタイル
├── utils/ # ヘルパー関数や汎用ユーティリティ
├── lib/ # ビジネスロジック、APIクライアント、データフェッチ関数など
├── next.config.js
├── package.json
└── tsconfig.json
各フォルダの役割とColocationの適用
app/ ディレクトリ内のColocation
app ディレクトリは、App Routerのルーティングと直接関連する部分です。Colocationの最も重要な適用箇所となります。
-
ルートセグメントフォルダ (
app/dashboard/,app/auth/login/):-
page.tsx: そのURLパスに対応するメインのページコンポーネントです。デフォルトでServer Componentとして動作します。 -
layout.tsx: そのルートセグメントと全ての子ルートに適用されるレイアウトです。これもServer Componentです。ネストされたレイアウトにより、特定のセクションに固有のUIシェルを定義できます。
-
-
ルートグループ
(folder):- 例:
app/(main)/ - URLパスに影響を与えずに、ルートを論理的にグループ化するために使用します。例えば、認証が必要なページを
(main)グループに入れ、認証不要なページを(auth)グループに入れるなど、目的やレイアウトで分割できます。これにより、開発者は関連するルートを一箇所で見つけやすくなります。
- 例:
-
プライベートフォルダ
_folder:- 例:
app/dashboard/_components/,app/auth/login/_components/ - アンダースコア (
_) で始まるフォルダは、ルーティングシステムから除外されます。これは、特定のルートセグメントにのみ関連するコンポーネント、フック、スタイルなどを配置するのに最適です。これにより、関連するコードが近くに保たれつつ、誤って公開ルートになることを防ぎます。
// app/dashboard/_components/DashboardHeader.tsx // このコンポーネントは /dashboard ルートでのみ使用されることを意図している import React from 'react'; export default function DashboardHeader() { return ( <header> <h2>Dashboard Overview</h2> {/* ... */} </header> ); } - 例:
api/ ディレクトリとRoute Handlers
-
app/api/users/route.ts:-
appディレクトリ内にapiセグメントを作成し、その中にRoute Handlers(旧API Routes)を配置します。これにより、APIエンドポイントの定義もルーティング構造と並行して管理でき、関連性が高まります。 -
route.tsファイルは、Web標準のRequest/ResponseAPI を使用して、GET, POST, PUT, DELETE などのHTTPメソッドを処理します。
// app/api/users/route.ts import { NextResponse } from 'next/server'; export async function GET(request: Request) { const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; return NextResponse.json(users); } export async function POST(request: Request) { const newUser = await request.json(); console.log('New user created:', newUser); return NextResponse.json({ message: 'User created successfully', user: newUser }, { status: 201 }); } -
アプリケーション全体で共有されるコンポーネント
Colocationはルート固有のファイルを近くに置くことを推奨しますが、アプリケーション全体で再利用されるUIコンポーネントは、appディレクトリの外に配置します。
-
components/ui/:- デザインシステムの一部となるような、汎用性の高いプリミティブなUIコンポーネント(例:
Button.tsx,Input.tsx,Modal.tsx)を配置します。これらは通常、"use client"ディレクティブを持つClient Componentとなることが多いでしょう。
- デザインシステムの一部となるような、汎用性の高いプリミティブなUIコンポーネント(例:
-
components/common/:- アプリケーション全体で共通のレイアウト要素(例:
Header.tsx,Footer.tsx)や、特定の機能に依存しない汎用コンポーネントを配置します。
- アプリケーション全体で共通のレイアウト要素(例:
その他の共有フォルダ
-
public/: 静的ファイル(画像、フォントなど)を配置します。 -
styles/: グローバルCSS、Tailwind CSSの設定、共有ユーティリティクラスなどを配置します。 -
utils/: アプリケーション全体で利用されるヘルパー関数、定数、型定義などを配置します。 -
lib/: データベースアクセス、外部APIクライアント、認証ロジックなどのビジネスロジックを配置します。Server Componentsから直接呼び出される関数などもここに置くと良いでしょう。// lib/data.ts export async function getDashboardData() { // サーバーサイドで直接データをフェッチ const res = await fetch('https://api.example.com/dashboard-data', { next: { revalidate: 3600 } // 1時間ごとにデータを再検証 }); if (!res.ok) { throw new Error('Failed to fetch dashboard data'); } return res.json(); }
Server ComponentsとClient Componentsの適切な配置
Next.js App Routerの重要な要素であるServer ComponentsとClient Componentsは、フォルダ設計においても考慮すべき点です。
-
Server Componentsをデフォルトとする:
- 特別な理由がない限り、すべてのコンポーネントはServer Componentとして扱うのがベストプラクティスです。状態管理、エフェクト、イベントハンドラ、またはブラウザAPIが必要な場合にのみ、ファイルの先頭に
"use client"を記述し、Client Componentとしてマークします。 - Server Componentは、
app/dashboard/page.tsxのように、データフェッチロジックを直接含めることができます。
// app/dashboard/page.tsx // デフォルトでサーバーコンポーネント import { getDashboardData } from '../../../lib/data'; // libからデータフェッチ関数をインポート export default async function DashboardPage() { const data = await getDashboardData(); return ( <div> <h1>Dashboard</h1> <p>Welcome, {data.user.name}!</p> {/* Client Component をここで使用することも可能 */} {/* <ChartDisplay data={data.charts} /> */} </div> ); } - 特別な理由がない限り、すべてのコンポーネントはServer Componentとして扱うのがベストプラクティスです。状態管理、エフェクト、イベントハンドラ、またはブラウザAPIが必要な場合にのみ、ファイルの先頭に
-
Client Componentsは最小限に、Colocationを意識して:
- Client Componentは、インタラクティブな部分にスコープを限定し、可能な限りルートの近く、または使用するServer Componentのすぐ隣に配置します。プライベートフォルダ
_componentsは、Client ComponentのColocationに最適です。
// app/auth/login/_components/LoginForm.tsx "use client"; // クライアントコンポーネントであることを明示 import { useState } from 'react'; export default function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); console.log('Login attempt:', { email, password }); // ログインロジック }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required /> <button type="submit">Login</button> </form> ); } - Client Componentは、インタラクティブな部分にスコープを限定し、可能な限りルートの近く、または使用するServer Componentのすぐ隣に配置します。プライベートフォルダ
Next.js App Router フォルダ設計でよくあるハマりどころと回避策
Next.js App Routerのフォルダ設計は強力ですが、いくつかの注意点があります。ここでは、開発者が直面しやすい問題と、その解決策を提示します。
1. Server ComponentsからのRoute Handlers呼び出しによる不要なネットワークホップ
ハマりどころ:
Server Componentsから、同じNext.jsアプリケーション内のRoute Handlersをfetchで呼び出すと、開発環境ではうまくいっても、本番環境で不要なネットワークホップが発生し、パフォーマンスが低下します。両方ともサーバー上で実行されるため、直接ロジックを呼び出す方が効率的です。
回避策:
Route Handler内に定義する予定だったロジック(外部API呼び出し、データベースクエリなど)を、lib/のような共有フォルダに抽出し、Server Componentから直接インポートして呼び出すようにします。
// 誤った例: Server Component から Route Handler を fetch
// app/page.tsx
export default async function Page() {
// 開発環境では localhost への fetch は成功するが、本番環境では外部ネットワークホップが発生する
// 常に完全なURL (例: 'https://your-domain.com/api/data') を指定しないとエラーになる可能性もある
let res = await fetch('http://localhost:3000/api/data');
let data = await res.json();
return <h1>{JSON.stringify(data)}</h1>;
}
// 正しい例: ロジックを直接呼び出す
// lib/data.ts
export async function getData() {
// 外部API呼び出しやDBアクセスなど、サーバーサイドで直接実行されるロジック
return { message: 'Data from server component logic' };
}
// app/page.tsx
import { getData } from '../lib/data'; // ロジックを直接インポート
export default async function Page() {
let data = await getData(); // ロジックを直接呼び出し
return <h1>{JSON.stringify(data)}</h1>;
}
2. "use client" ディレクティブの過剰な使用
ハマりどころ:
必要のないコンポーネントに"use client"を適用すると、そのコンポーネントツリー全体がクライアントバンドルに含まれてしまい、バンドルサイズが増加し、アプリケーションの初期ロードパフォーマンスが低下します。
回避策:
状態、エフェクト、イベントハンドラ、またはブラウザAPIを直接使用するコンポーネントにのみ"use client"を使用します。可能な限りServer Componentsをデフォルトとし、Client Componentsは小さく、インタラクティブな部分に限定するようにフォルダ設計を意識します。_componentsフォルダは、このようなClient Componentの配置に役立ちます。
3. Client ComponentsでのサーバーサイドAPIの利用
ハマりどころ:
headers()やcookies()のようなサーバーサイドユーティリティをClient Componentsで直接使用しようとするとエラーが発生します。これらの関数はサーバー環境でのみ実行可能です。
回避策:
Client Componentsで現在のルートに関する情報にアクセスするには、usePathname、useSearchParams、useRouterといったクライアントフックを使用します。サーバーサイドのデータが必要な場合は、Server ComponentでフェッチしてClient Componentにpropsとして渡すか、Client ComponentからRoute Handler(API)を呼び出します。
4. ハイドレーションエラー
ハマりどころ:
サーバーでレンダリングされたマークアップとクライアントでレンダリングされたマークアップが一致しない場合に発生します。これは、不適切なHTMLタグのネスト、クライアント側でのみレンダリングされる要素、またはサーバーとクライアントで異なるデータが使用されるなどが原因で起こります。
回避策:
- アプリケーション全体のマークアップを確認し、不適切なタグで要素やカスタムコンポーネントを囲んでいないことを確認します(例:
<div>を<p>で囲む)。 - クライアント側でのみレンダリングしたい要素は、
useEffectフック内でマウント後にレンダリングするか、suppressHydrationWarningプロパティを使用して一時的に警告を抑制します(根本解決ではないため注意)。
5. Context Providerの不適切な配置
ハマりどころ:
Context ProviderをServer Components内に直接配置することはサポートされていません。また、必要以上にアプリケーションツリーの浅い位置に配置すると、Next.jsの最適化が十分に機能しない可能性があります。
回避策:
Context ProviderはClient Componentで定義し、Next.jsが可能な限り最適化できるように、アプリケーションツリーのできるだけ深い位置に配置します。例えば、ThemeProviderが/dashboard/settingsルートでのみ使用される場合、そのルートの最も近いlayout.tsxファイルで、"use client"を持つラッパーコンポーネントを作成し、その中でプロバイダーをレンダリングします。
// app/dashboard/settings/layout.tsx
import ThemeProvider from './_components/ThemeProviderWrapper'; // Client Componentとして定義
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider> {/* Client Component の中で Provider を使用 */}
<nav>Settings Nav</nav>
{children}
</ThemeProvider>
);
}
// app/dashboard/settings/_components/ThemeProviderWrapper.tsx
"use client";
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext<any>(null);
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
// ...テーマロジック
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
まとめ:Next.js App RouterにおけるColocationフォルダ設計のベストプラクティス
Next.js App Routerを用いた大規模アプリケーション開発において、Colocationに基づいたフォルダ設計は、コードの保守性、可読性、そしてスケーラビリティを向上させるための鍵となります。
この記事で解説した主要なポイントは以下の通りです。
-
機能ごとのColocation: ルートセグメントごとに
page.tsx、layout.tsx、そしてプライベートフォルダ_componentsを活用し、関連ファイルを併置する。 -
Server Componentsをデフォルトに: インタラクティブ性が必要な場合にのみ
"use client"を使い、Client Componentsのスコープを最小限に抑える。 -
ルートグループとプライベートフォルダの活用:
(folder)でURLに影響なくルートを整理し、_folderでルーティング対象外のファイルを隔離する。 -
共有コンポーネントの分離: アプリケーション全体で再利用されるUIは
components/uiやcomponents/commonに集約する。 -
ロジックの分離: ビジネスロジックやデータフェッチ関数は
lib/に、汎用ユーティリティはutils/に配置し、Server Componentsから直接呼び出す。 -
よくあるハマりどころの理解: Server ComponentsからのRoute Handlers呼び出し、
"use client"の過剰使用、Context Providerの配置など、具体的な問題とその回避策を把握する。
これらのベストプラクティスをプロジェクトに適用することで、Next.js App Routerの恩恵を最大限に享受し、大規模なアプリケーションでも秩序だった開発を維持できるでしょう。
さらに深く学びたい方は、Next.js 公式ドキュメントの Project Organization を参照し、最新の推奨事項を確認することをお勧めします。