こんにちは、かつコーチです。
前回は「Module not found: next/router」というエラーへの対処法を紹介しました。
つまずき系のTipsが続いたので、今回は少し視点を変えて、設計の話をします。
Next.jsのApp Routerは自由度が高い分、ディレクトリ構成を何も考えずに進めると、すぐに見通しの悪いプロジェクトになってしまいます。
今回は、実務でよく使われるディレクトリ構成のパターンと、それぞれのメリット・デメリットを整理します。
前提知識・何が起きるか
App Routerでは、appディレクトリの中のフォルダ構造がそのままURLのルーティングになります。
そのため「ルーティングに使うフォルダ構造」と「コンポーネントやロジックを整理するためのフォルダ構造」をどう共存させるかが、設計上の悩みどころになります。
何も考えずに進めると、次のような問題が起きがちです。
- 1つのコンポーネントが複数ページで使われているのに、特定のページフォルダの中に置かれていて見つけにくい
- ビジネスロジックとUIコンポーネントが同じファイルに混在し、テストしづらい
- 機能追加のたびに
componentsフォルダが肥大化し、どこに何があるか分からなくなる
これらを防ぐために、App Routerには「Route Groups」や「Colocation(配置の近接化)」といった仕組みが用意されています。
基本の書き方・実装手順
パターン1: 機能単位でグルーピングする(Feature-based)
小〜中規模のプロジェクトでよく使われるのが、機能単位でディレクトリを分ける構成です。
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── posts/
│ │ ├── page.tsx
│ │ ├── [id]/
│ │ │ └── page.tsx
│ │ └── _components/
│ │ ├── PostList.tsx
│ │ └── PostCard.tsx
│ └── settings/
│ ├── page.tsx
│ └── _components/
│ └── ProfileForm.tsx
├── features/
│ ├── posts/
│ │ ├── api.ts
│ │ ├── types.ts
│ │ └── hooks.ts
│ └── auth/
│ ├── api.ts
│ └── types.ts
└── components/
└── ui/
├── Button.tsx
└── Card.tsx
ポイントは、app配下にアンダースコアで始まる_componentsのようなプライベートフォルダを作ることです。
App Routerでは、フォルダ名の先頭にアンダースコアを付けると、そのフォルダはルーティングの対象から除外されます。
これにより「そのページ専用のコンポーネント」と「ルーティングに使うファイル」を同じ階層に共存させつつ、URLには影響を与えずに済みます。
パターン2: Route Groupsでレイアウトを分岐させる
認証が必要なページと不要なページで、レイアウトを分けたいケースはよくあります。
このようなときは、括弧で囲んだフォルダ名を使うRoute Groupsが便利です。
app/
├── (auth)/
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
├── (dashboard)/
│ ├── layout.tsx
│ ├── posts/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
└── layout.tsx
括弧付きのフォルダ名はURLに含まれないため、(dashboard)/posts/page.tsxは/postsとしてアクセスできます。
// ❌ Before: 全ページで同じlayoutを使い回している
// app/layout.tsx がヘッダー・サイドバー・フッターを全部含んでいる
// ✅ After: Route Groupsで認証ページとダッシュボードのlayoutを分離
// app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <div className="auth-container">{children}</div>;
}
// app/(dashboard)/layout.tsx
import { Sidebar } from "@/components/Sidebar";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-container">
<Sidebar />
<main>{children}</main>
</div>
);
}
ログイン画面にサイドバーを表示したくない、といった要件を、余計な条件分岐なしで実現できます。
パターン3: 共通ロジックはlibやserverフォルダに集約する
Server Actionsやデータベースアクセスなど、複数の機能から呼ばれる処理は、機能フォルダの外に共通ディレクトリとして切り出しておくと再利用しやすくなります。
src/
├── lib/
│ ├── db.ts
│ ├── auth.ts
│ └── validation.ts
├── server/
│ └── actions/
│ ├── posts.ts
│ └── users.ts
└── app/
└── ...
server/actionsのように、Server Actionsをまとめるフォルダを作っておくと、どのファイルがサーバー専用の処理かが一目で分かるようになります。
つまずきポイント・一次情報の体験談
私が実際に経験した失敗を1つ共有します。
あるプロジェクトで、最初はcomponentsフォルダに全てのコンポーネントをフラットに置いていました。
開発初期は10個程度だったので問題なかったのですが、機能追加を重ねるうちに100個近くまで膨れ上がりました。
components/
├── Header.tsx
├── PostCard.tsx
├── PostList.tsx
├── PostForm.tsx
├── UserAvatar.tsx
├── UserSettingsForm.tsx
├── CommentList.tsx
├── CommentForm.tsx
... (以下90個以上)
新しいメンバーが「コメント関連の修正をしたいが、どのファイルを触ればいいか分からない」と聞いてくるようになり、探索コストが目に見えて増えていました。
途中から機能単位のフォルダ構成にリファクタリングしたのですが、既存のimportパスが大量に散らばっていたため、修正だけで丸2日かかりました。
このとき学んだのは、ディレクトリ構成は「後から直せばいい」と軽く考えず、機能が10個を超えたあたりで一度見直すべきだということです。
最初から完璧な構成を目指す必要はありませんが、「同じ機能に関するファイルは近くに置く」という原則だけは早い段階で決めておくと、後々の手戻りを大きく減らせます。
まとめ
この記事のポイント
- App Routerでは
app配下の構造がそのままURLになるため、ルーティングと整理のバランス設計が重要 - フォルダ名にアンダースコアを付けると、ルーティング対象から除外できる(Colocationに便利)
- 括弧付きのフォルダ名(Route Groups)でURLに影響を与えずにlayoutを分岐できる
- Server Actionsや共通ロジックは
libやserverのような専用ディレクトリに集約すると見通しが良くなる - ディレクトリ構成の見直しは後回しにするほどコストが増えるため、機能数が増えてきた段階で早めに検討する
次に読むべき記事
次回はいよいよNext.jsシリーズ最終回です。
Server Actionsを使ったシンプルなブログアプリを実際に作りながら、これまで学んだ内容を総まとめします。