はじめに
Next.js(App Router)でプロダクトを作っていると、機能が増えるほど「このファイルどこに置くんだっけ?」という小さな迷いが積み重なっていきます。最初は気にならないのですが、ページが20、30と増えてくると、一気に問題が表面化します。
- どこに何があるか分からず、grep頼みでファイルを探す
- 似たようなユーティリティが utils のあちこちに重複する
- 「とりあえず components に入れる」が積もって肥大化する
- サーバー専用のコードをうっかりクライアントから import してビルドが壊れる
これらはコードの良し悪しというより、ディレクトリ設計=置き場所のルールが曖昧なことが原因です。
この記事では、私が実際に運用しているNext.js(v16 / App Router / TypeScript)のサービスを題材に、Next.jsのディレクトリ構成をどう決めたか、そしてなぜその構成にしたのかという設計意図を、実コードを交えて公開します。「Next.js フォルダ構成」で検索して出てくる一般的なテンプレとは少し違う、実務で角が取れた形になっているはずです。
今回のディレクトリ構成
まず全体像です。src/ 配下はこうなっています。
src/
├── app/ # App Router。URLと1対1のルーティング
│ └── actress/
│ ├── page.tsx # エントリ + generateMetadata
│ ├── page.server.tsx # SSR:データ取得・リダイレクト・JSON-LD
│ ├── page.client.tsx # "use client":状態管理・ルーティング
│ ├── page.content.tsx # 純粋な描画(props のみ)
│ ├── page.schema.ts # zod スキーマ
│ └── _components/ # このページ専用のコンポーネント
├── api/ # バックエンド通信。操作1つにつき1フォルダ
│ ├── ActressRead/
│ │ ├── actressApi.ts # fetch関数(サーバー/クライアント共用)
│ │ └── useActressData.ts # クライアント用フック
│ └── Shared/ # apiClient, http, 共通型, BFFヘルパー
├── components/ # ページをまたいで使う共通UI
├── lib/ # 横断的なロジック・hooks・定数・テーマ
├── stores/ # Zustand のグローバルストア
├── generated/ # 自動生成ファイル(ルート一覧など)
└── ad/ # 自動生成された広告バナーHTMLポイントは、いわゆる features / services / hooks / utils / types といった「種類で切るフォルダ」をあえて作っていないことです。代わりに「ページ」と「API操作」という2つの軸で切っています。理由は後述します。
各ディレクトリの役割
app — URLと1対1のルーティング層
App Router の app/ は、私の設計では1ファイル=1責務になるよう、ページを4つのファイルに分割しています。これがこの構成のいちばんの肝です。
app/actress/
├── page.tsx # 入口。メタデータ生成だけ
├── page.server.tsx # サーバーコンポーネント。fetch・redirect・構造化データ
├── page.client.tsx # "use client"。状態・URL同期・データフック呼び出し
└── page.content.tsx # 受け取った props を描画するだけそれぞれの役割を厳密に決めています。page.tsx は generateMetadata を返す入口で、中身は page.server.tsx に丸投げします。
export default async function ActressPage({ searchParams }: Props) {
return <ActressServer searchParams={searchParams} />;
}page.server.tsx はサーバー側だけで動き、URLの検索条件をもとに SSR でデータを取得し、JSON-LD(構造化データ)まで組み立てます。SEOを意識するなら、ここでクローラがJSなしで内容を把握できるHTMLを返すのが効きます。
page.client.tsx は "use client" を付け、状態管理・URL同期・データ取得フックの呼び出しを担当。そして page.content.tsx は props を受け取って描画するだけの純粋なコンポーネントにしています。
// router も fetch も持たない
export const ProductContent = ({ productData, query, ... }: Props) => {
// 副作用ゼロ。Storybook やテストでは props を差し替えるだけで全状態を再現できる
};副作用を持たないので、Storybook やテストでは props を差し替えるだけであらゆる状態(ローディング・エラー・空・正常)を再現できます。「サーバー専用コード」「クライアント専用コード」「純粋な見た目」が物理的に別ファイルに分かれているので、"use client" の付け忘れや、サーバー専用 import の混入が起きにくいのも利点です。
ページ専用のコンポーネントは _components/ に閉じ込めます。アンダースコア始まりなので App Router のルーティング対象から外れ、「このページでしか使わない」ことがディレクトリ名から一目で分かります。
api — 操作1つにつき1フォルダ
私は API 通信を services という1つのフォルダにまとめず、操作単位(ユースケース単位)で1フォルダにしています。ActressRead / ActressCreate / ImgDelete / AuthLogin のように、動詞+名詞で命名します。
api/ActressRead/
├── actressApi.ts # 純粋な fetch 関数。"use client" を付けない
└── useActressData.ts # それをラップしたクライアント用フックactressApi.ts には "use client" を付けず、サーバーコンポーネントからもクライアントからも呼べる純粋な fetch 関数を置きます。クライアント専用の状態ロジックだけを useActressData.ts のフックに分離します。共通部分(HTTPクライアント、レスポンス封筒の開封、BFFプロキシ経由の設定など)は api/Shared/ に集約します。
「種類(hooks/services/types)で切らず、操作で切る」ことで、ある機能をいじるときに関連ファイルが1フォルダに揃うのが最大のメリットです。
components — ページ横断の共通UI
2ページ以上で使う UI だけを components/ に置きます。MyPagination / SearchField / UserAvatar のような汎用部品です。逆に言うと、1ページでしか使わないものは絶対にここに入れず app/.../_components/ に置く、というルールを徹底しています。これだけで components/ の肥大化が止まります。
lib — 横断ロジック・hooks・定数・テーマ
ここが一般的なテンプレと違う部分です。私は hooks/ utils/ constants/ を独立させず、横断的に使う小さなロジックをまとめて lib/ に入れています。
lib/
├── const.ts # 定数
├── theme.ts # MUIテーマ
├── seo.ts # JSON-LD生成など
├── useMedia.ts # 汎用フック
├── useRestoreListScrollOnBack.ts # 戻る時のスクロール復元
└── scroll.ts / common.ts ... # 雑多なユーティリティ理由は、小規模〜中規模では hooks と utils と constants を分けても1ファイルずつしか入らないフォルダが量産されるだけだからです。use で始まればフック、それ以外はユーティリティ、と命名規則で十分判別できます。
stores — Zustand のグローバルストア
ログインユーザーなど、本当にアプリ全体で共有したい状態だけを Zustand のストアにして stores/ に置きます。
export const useUserStore = create<UserState>((set) => ({
user: undefined,
setUser: (user) => set({ user }),
}));ストアは3つだけ。「URLで表現できる状態はURLに」「サーバーから取れるものはサーバーで」を優先し、グローバルストアは最後の手段にしています。
types / utils / constants / providers がない理由
冒頭で触れた通り、これらの単独フォルダは作っていません。
- types:型はそれを使う api/.../*.ts の中で export type し、ドメイン型だけ api/Shared/ に置く。型と実装を引き離さない方が追いやすい
- utils / constants:lib/ に集約(前述)
- providers:プロバイダは App Router の app/layout.tsx に直接書くため専用フォルダは不要
「フォルダは必要になってから作る」が私の方針です。
generated / ad — 自動生成の隔離
generated/ にはスクリプトが吐くルート一覧、ad/ には広告バナーHTMLが入ります。手で編集してはいけないファイルを物理的に隔離し、ファイル冒頭に「AUTO-GENERATED ... do not edit」と明記しています。生成物と手書きコードが混ざらないだけで、レビューの安心感がまるで違います。
この設計で意識したこと
単一責任の原則と責務の分離
page.{server,client,content}.tsx の分割は、まさに単一責任の原則をファイル境界で強制する仕組みです。「データ取得」「状態管理」「描画」が混ざったコンポーネントは、テストもレビューも辛くなります。ファイルを分けてしまえば、各ファイルが何をするかを名前が保証してくれます。
スケールしやすさ
「種類で切る(hooks/services/...)」設計は、機能が増えるほど横に広がり、1機能の変更で複数フォルダを横断する羽目になります。私の「ページ軸 + 操作軸」は、機能が増えても触る範囲が1ディレクトリに収まりやすいので、規模が大きくなっても破綻しにくい構成です。
チーム開発しやすさ
置き場所のルールが「2ページ以上で使うか?」「サーバー/クライアント/描画のどれか?」といったYes/Noで答えられる問いに落ちているので、新しく入った人でも迷いません。命名規則(useXxx=フック、XxxRead=取得API)も判断を支えます。
良かった点
実際に運用して効果を感じたのは次の3つです。
- ファイルを探さなくなった。「actress一覧のSSR」なら app/actress/page.server.tsx 以外にありえない。場所が構成から逆算できる
- "use client" 事故が激減した。サーバー/クライアントが別ファイルなので、境界をうっかり越えにくい
- テストとStorybookが書きやすい。page.content.tsx が純粋なので、props を流すだけで全状態を再現できる
改善したい点
完璧な設計はないので、見直したい点も正直に書きます。
- lib/ が雑多になりがち。seo.ts のような重めのロジックと小さなユーティリティが同居しているので、肥大化したら lib/seo/ のようにサブフォルダで切り直したい
- ページ4分割は小さなページにはオーバー。状態を持たない静的ページにまで page.client.tsx を用意するのは過剰なので、規模に応じて省略する判断ルールを明文化したい
- api/ のフォルダが操作ごとに増える。数が増えてきたらドメイン(api/actress/Read 等)でグルーピングし直す余地がある
設計は「今の規模で迷わないこと」が正解で、規模が変われば最適解も変わります。そこは割り切っています。
まとめ
私のNext.jsディレクトリ設計のポイントは3つです。
- ページは server / client / content に分割して責務を物理的に分ける
- API は操作単位で1フォルダにまとめ、種類で切らない
- フォルダは必要になってから作る(hooks / utils / types を先回りして作らない)
「Next.js のディレクトリ構成」「フロントエンドのディレクトリ設計」に唯一の正解はありませんが、置き場所をYes/Noの問いで決められる状態にすることがいちばん大事だと考えています。この記事が、自分のプロジェクトの構成を見直すきっかけになれば嬉しいです。




