Next.jsで Bulletproof-react と Service層・Repository層を取り入れたディレクトリ設計

2025-04-09

2025-04-09

11 min read

Bulletproof-reactNext.jsReactTypeScript

はじめに

昨今のSupabaseなどのBaaS(Backend as a Service)+ Prismaを用いた構成や、Next.js 14 でのServer Actionの登場により、ディレクトリ設計が複雑になっていると感じませんか?

また従来より複雑に感じる要因として、Client ComponentsServer Componentsをうまく統合するようにコンポーネント設計が必要であることが挙げられます。

そして設計をミスると修正するのに多大な工数がかかるし、そもそも納期などの関係で修正するタイミングも無かったりして、そのまま開発が進んでいくようなこともあると思います。

今回Next.jsにおけるディレクトリ設計やアーキテクチャーを改めて整理したので、今後の自分のためにもここにまとめておこうと思います。

前提としては

  • Server Actionを利用する
  • フロントエンドとバックエンドともNext.jsで開発を行う
  • Bulletproof-reactをベースにContainer / Presentationパターンを意識した設計を導入する
  • Service層Repository層を導入する
  • 小規模だとオーバーエンジニアリングになる部分もあるかもだが、スケールをしても耐えれるような構成を目指す

また現段階ではNext.js 15 、React 19 までの開発を想定しています。

構成について

全体の構成

まず全体構成としては以下のディレクトリ設計になります。

.
├── prisma       # Prisma のスキーマやマイグレーションファイルを管理
├── public        # 静的ファイル(画像、フォントなど)を配置
└── src/          # アプリケーションのソースコード
    ├── app/       # Next.js App Router のルーティングとページ
    ├── components/   # 再利用可能な共通の UI コンポーネント
    ├── constants/      # 定数や設定値を定義(例:日付フォーマット、メッセージなど)
    ├── features/     # ドメイン単位の機能モジュール(機能ごとのUI/ロジック)
    ├── hooks/      # カスタム React Hooks
    ├── lib/          # 外部ライブラリとの連携や共通処理(API クライアント、認証など)
    ├── repositories/    # データ取得・永続化処理(DBやAPIとのやり取り)
    ├── services/     # ビジネスロジック(use case)を実装
    ├── types/       # 型定義(モデル型、ユーティリティ型など)
    └── utils/      # 汎用的なユーティリティ関数

アーキテクチャーはBulletproof-reactをベースに考えています。featuresの中身については別途後述します。

私はReactにおけるSPA開発ではAtomic Designを利用することが多かったのですが、昨今のNext.jsの開発における「コードの保守性と再利用」「複数人での開発」などを考えると、Bulletproof-reactの方が堅牢な設計・実装パターンを実現しやすいと感じております。

またService層Repository層を導入することで、PrismaでのDBのアクセスやAPIを叩くロジックなどを隠蔽し、保守や再利用がを行いやすい設計になると思います。

DBのテーブルの型は「types/models」以下に「/userType.ts」のようにテーブル毎にまとめて定義しておき、全てのファイルではここの型をimportして利用するようにします。Propsの定義で必要な場合なども全てここから取り出します。

featuresディレクトリ内の構成

featuresディレクトリ内の構成は以下です。

.
└── src/features
    └── domain/       # ドメイン単位:例)user/
        └── feature      # 機能単位:例)user-table/
            ├── actions/          # Server Actionの処理
            ├── components/  # 再利用可能なUI部品であるPresentational Component
            ├── constants/        # 定数
            ├── containers/      # 状態管理やロジックを含んだContainer Component。UIにデータやイベントを渡す役割。
            ├── hooks/            # カスタムフック
            ├── providers/     # 状態管理などのラッパーとか
            ├── schemata/        # Zodなどによるバリデーションスキーマやフォーム構造定義
            ├── types/          # 型定義
            ├── utils/      # 汎用的なユーティリティ関数
            └── index.tsx     # 機能のエントリーポイント

featuresディレクトリ内は直下に全てのfeature(機能単位)を配置するとスケールした時に把握できなくなりそうなので、domainで区切るようにしました。

feature内の各ディレクトリはこういったディレクトリができるケースがあるという意味で、全て必要なわけではありません。必要なものだけ作成するイメージです。

ここで大きくポイントになるのは2つです。

  1. Container / Presentationパターンを意識すること
  2. index.tsxをエントリーポイントにすること

1.Container / Presentationパターンを意識すること

Next.jsではClient ComponentsServer Componentsが入り乱れることになるので、Container層Presentational層を分けることで管理しやすくなります。
またテストが容易になったり、storybookなども使用しやすくなるメリットがあります。
例えばContainer層ではデータの取得を行い、Presentational層では取得したデータを受け取って表示やユーザー操作によってのアクションを提供したりします。
詳しく話すと長くなるので今回は割愛させていただき、別途記事にする予定です。

2.index.tsxをエントリーポイントにすること

page層や別のfeatureから読み込む時はこのエントリーポイントからexportしたファイルのみimport可能にすることで、各ファイルの管理をしやくしなります。
エディターやeslintの設定でエントリーポイントからexportしていないファイルを直接読み込めばエラーが出るように設定なども可能です。
私のディレクトリ設計では以下の記述をeslint.config.mjsに記述することで一括で適応しています。
一括ではなく個別で適応したりも可能ですが、私は毎回記述するのが大変に感じたため一括でできるように設計の方でカバーしています。

// eslint.config.mjs
const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: ["@/features/*/*/*"], // エントリーポイントをからimportしないとエラーにする
        },
      ],
    },
  },
]

featuresディレクトリの構成の具体例

例えば「user-table」というfeature(機能)を作りたいとします。

すると以下のような構成になります。

.
└── src/features
    └── user/
        └── user-table
            ├── components/
            │   └── UserTable
            │       ├── index.tsx
            │       ├── index.stories.tsx
            │       └── index.test.tsx
            ├── containers/
            │   └── UserTableContainer
            │       ├── index.tsx
            │       └── index.test.tsx
            └── index.tsx

先ほどの述べたContainer / Presentationパターンで設計をし、コンポーネントはディレクトリ名で表しその中にindex.tsxを配置します。

このコンポーネントに対してテストやストーリーブックのファイルを作成したい場合などを考慮してこのような構成しています。

今回は例なので簡単な内容にしていますが、機能が大きくなると各ディレクトリやファイル数は増えることが想定されます。

また余談ですが、VSCodeでindex.tsxのファイル名を開くと表示されるファイル名からでは何のファイルを開いているかがわかりにくくなってしまいます。

ですが設定で「index.tsxの場合はディレクトリ名を表示する」といったことが可能です。

私は以下の内容をsettings.jsonに記述して、この問題を回避しています。

"workbench.editor.customLabels.patterns": {
    "**/index.*": "${dirname} .../${dirname(1)}",
    "**/{page,layout,template,route,actions,hooks,components,utils,types}.{js,ts,jsx,tsx}": "${dirname}/${filename}.${extname} .../${dirname(1)}"
  }

まとめ・補足

この設計では機能ごとにスコープを作って開発していくので、修正なども行いやすかったり、複数人での開発も行いやすいとメリットがあります。

しかし小規模なプロジェクトでもファイル数を分けて開発していく必要があるので、チーム内でルールを明確にする必要があります。

また使用するライブラリなどとの相性もあるように感じます。

私が開発を行うにあたって利用したツールやライブラリなどを列挙しておきます。

※ 技術の移り変わりが早いので、情報が古いものと入り乱れていたり、AIも追いつけていない状況が散見されます。  利用する際は公式ドキュメントを参照するのがおすすめです。  今回の設計に関係ないツールも技術選定の参考になればと思い記述しています。

  • Tailwind CSS v4 + shadcn/ui:CSSフレームワークとコンポーネントライブラリ
  • conform + zod + useActionState:フォーム周り
  • nuqs(旧称next-usequerystate):URLのクエリパラメータの状態管理
  • clerk:認証周り
  • supabase:DB
  • Prisma:ORM

実務ではプロジェクトの規模や要件、今後の展望によって変わる部分もあると思いますが、一例として参考になれば幸いです。

実装を含めた具体例はまた別の記事にまとめていきたいと思います。