初めまして、テックドクターでフロントエンド開発を担当している大瀧です。
ディレクトリ構成はコードの可読性やスケーラビリティに関わる重要な要素であると思っています。
しかし、フロントエンドのディレクトリ構成はベストプラクティスが確立されておらず、わりと悩むポイントです。
そこで今回は、Next.jsのApp routerにおいて、弊社で採用しているディレクトリ構成を共有します。この記事がディレクトリ構成に悩む開発者の助けになれば幸いです。
ディレクトリ構成の自由度が高すぎる問題
さきほど「フロントエンドのディレクトリ構成はベストプラクティスが確立されていない」と書きましたが、特にApp routerのディレクトリ構成については、公式ドキュメントで以下のように記載されています。
There is no "right" or "wrong" way when it comes to organizing your own files and folders in a Next.js project.
Next.js プロジェクトでファイルやフォルダを整理する方法には「正しい」「間違っている」はありません。
引用元:Routing: Project Organization | Next.js(日本語訳:プロジェクト編成とファイル・コロケーション | Next.js 公式ドキュメント 日本語翻訳プロジェクト)
正解も不正解もないと明記されているものの、これは自由度を高める一方で、開発者からすると悩ましいです。実際のプロジェクトにおいては、ある程度ルール化しておかなければコードの品質が保てませんし、保守しづらいコードは開発速度低下を招いてしまいます。
そこで本記事では、テックドクターで私が手がけているHealth Portalというプロダクトの構成を共有します。Health Portalは、クライアント(ヘルスケアアプリやSaMDを開発したい企業)がデジタルバイオマーカー*1を利活用するためのプラットフォームです。JavaScriptのフレームワークにNext.js(App router)を採用しています。
Health Portalの技術スタック
前提として、フロントエンドで採用している主なライブラリはざっくり以下のとおりです。
- Next.js v14
- Tailwind CSS
- React Hook Form
- zod
- keycloak.js
弊社のディレクトリ構成とその解説
ディレクトリ構成と各ディレクトリの役割
まず、ディレクトリ構成の全体像は以下のとおりです。
src | +-- app # Routing files(Next.jsが標準で提供するファイル) | +-- components # 横断的(ドメインに依存しない)なUIコンポーネント | +-- features # 特定のドメイン・機能に関係するコンポーネント | +-- hooks # ドメインに依存しない、横断的なhooks | +-- providers # アプリケーションプロバイダー | +-- utils # 横断的な汎用関数 | +-- constants # 横断的な定数 | +-- types # 横断的な型定義 | +-- styles # スタイリング(css)に関するファイル | +-- lib # ライブラリの処理や標準処理を共通化したコード | +-- tests # 自動テスト関連
- app: ルーティングの責務を持つディレクトリ。Routing files(Next.jsが標準で提供するファイル。page.tsxやlayout.tsxなど)のみを配置します
- components: ボタンやモーダルなど、再利用可能でドメインに依存しないUIコンポーネントを格納します。
- features: 特定の機能に関連するファイルを集約し、機能ごとのサブディレクトリに格納しています。例えば、ユーザー管理機能に関連するコンポーネント、hooks、ユーティリティ関数、型定義などをこのディレクトリ内でまとめます。
- hooks: グローバルに使用されるカスタムフックを格納します。ドメインに依存しない汎用的なロジックはここにまとめます。
- providers: ReactのContext APIやその他のプロバイダーを配置します。グローバルな状態管理やテーマ設定など、アプリケーション全体で使用されるプロバイダーをここに集約します。
- utils: 簡潔で再利用可能なユーティリティ関数を格納します。これには、日付処理、文字列操作、API通信など、特定のドメインに依存しない関数が含まれます。
- constants: アプリケーション全体で使用される定数を格納します。
- types: TypeScriptの型定義をまとめます。
- styles: グローバルスタイルやテーマ設定、Tailwind CSSのカスタマイズを含むスタイルシートをここに配置します。
- lib: プロジェクト内で再利用されるライブラリや、外部ライブラリの実装をここに格納します。
- tests: ユニットテストや統合テストなど、自動テストに関連するファイルを格納します。
基本的には、Reactアプリ開発のベストプラクティスと言われるbulletproof-reactを参考にしてディレクトリ構成を考えています。
bulletproof-reactについてはmejinさんの記事がとてもわかりやすくまとまっているのでご参照ください。
Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件
肝となるのがfeaturesディレクトリで、ここには機能ごとのファイルが格納されます(詳細は後述)
以前はコンポーネントの粒度毎にディレクトリを分割するAtomic Designが流行ってた印象ですが、ドメイン毎にディレクトリを分類するほうが、フレームワークへの依存度が少なく学習コストやフレームワークの移行ハードルが低いように思います。
またClean Architectureの著者であるRobert C. Martinさんも記事の中で以下のように述べています。
Your architectures should tell readers about the system, not about the frameworks you used in your system.
あなたのアーキテクチャが読み手に伝えるべきなのはシステムのことであって、使用したフレームワークのことではありません。
引用元:Clean Coder Blog(日本語訳は筆者)
featuresの切り方について
featuresディレクトリの構成は、名のとおり機能毎にディレクトリを分割しています。
ディレクトリ分割の判断基準としては、オブジェクト指向UIというUI設計思想に基づき、分類しています。 オブジェクト指向UIとは、簡単にいうとオブジェクトを起点にユーザーのアクションが設計されたUIのことで、ここでいうオブジェクトとは「ユーザーが関心を寄せるモノ、ユーザーの操作対象となる名詞」を指します。このオブジェクト(ユーザーの関心事)ごとにディレクトリを分割しています。
例えば、「ユーザーの作成」や「ユーザーの削除」等のアクションがある場合、オブジェクトは「ユーザー」となり、関連するソースコードはuserディレクトリに格納することになります。 オブジェクト指向UIの詳細については、ぜひこちらを読んでみてください。
オブジェクトごとのディレクトリ配下の構造についても、基本的にbulletproof-reactに則っており、下記の図のとおり特定のドメインに依存するコンポーネントを格納します。
features | +-- user # 特定のドメインに関するディレクトリ | | | +-- hooks # 特定のドメインに関するhooks | +-- utils # 特定のドメインに関する汎用関数 | +-- components # 特定のドメインに関するコンポーネント | +-- types # 特定のドメインに関する型 | +-- providers # 特定のドメインに関するプロバイダー | +-- pages # appディレクトリ配下から呼び出されるページコンポーネントを格納する | +-- users_page.tsx | +-- user_new_page.tsx | +-- ... +-- ...
しかし、オブジェクトごとのディレクトリすべてが完全に同じ構造を持つ必要はなく、その機能に必要なディレクトリのみを作成します。
appディレクトリとfeaturesディレクトリの対応関係について
features/xxx/pages
に格納されたページコンポーネントがエントリポイントとなり、appディレクトリ配下の対応するpage.tsx
から呼び出されます。
app | +-- users | | | +-- page.tsx # `features/user/users_page.tsx`を呼び出す | features | +-- user | | | +-- pages | +-- users_page.tsx | +-- ... +-- ...
細かなルールについて
異なるfeature間での依存はNG
各featureは独立して機能するべきであり、他のfeatureに依存しないことで低結合・高凝集なソースコードを実現することができるため、異なるfeature間での依存はNGとしています。
横断的なコンポーネントに特定ドメインの要素を含ませるのはNG
コンポーネントを汎用化し、かつfeatureの凝集度を高めるために、横断的なコンポーネントに特定ドメインの要素を含めるのはNGとしています。
簡単な例ですが、以下のようにButtonコンポーネントにuserドメインを含めると汎用性が落ちてしまいます。
type ButtonProps = { user: { role: 'admin' | 'user'; }; } & ButtonHTMLAttributes<HTMLButtonElement>; const Button: FC<PropsWithChildren<ButtonProps>> = ({ user, children, ...props }) => { const buttonClass = user.role === 'admin' ? 'bg-red-500' : 'bg-blue-500'; return ( <button {...props} className={buttonClass}> {children} </button> ); };
本構成のメリット
コードリーディング時の認知負荷が下がる
単一の機能毎にディレクトリが分かれているため、コンテキストが統一されコードの理解がし易いと個人的には思っています。
他の開発者とコンフリクトしづらくなる
機能毎にディレクトリが分かれているため、機能修正や追加の際にコンフリクトが起きにくくなります。大規模な開発体制の場合は開発生産性が結構上がるんじゃないかと思います。
デメリット・改善点
やっぱりfeaturesディレクトリ配下の切り方が曖昧
オブジェクト指向UIの考え方を参考にfeatureを分類することで、開発者間で一応は共通認識を持つことは出来ているような気はしつつ、明確な線引があるわけではないのでたまに悩んでしまいます。ラクスさんの場合は画面仕様書単位で切り分けているようです。
最終的な画面表示のロジックはfeatures/xxx/pages
ではなく、app配下で実装してもいいかも
features/xxx/pages
を作成すると責務の分離ができる反面、該当のページがどのURLに対応するかが分かりづらくなってしまいます。
なので、pageのレンダリング処理に関してはapp配下で実装してもいいかもしれないです。
実際、元VercelのエンジニアであるSteven Teyさんが開発している、dubというプロダクトではapp配下にpage.tsx
とpage-client.tsx
を格納する設計になってます(以下を参照ください)
dub/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/tokens at 35973de925857f4952d7910a2424ffab478ad2cb · dubinc/dub · GitHub
正直なところ、ルーティングとレンダリングの責務を分離することに今のところ大きなメリットを感じていないので、こっちのほうが分かりやすいかも知れません。