はじめに
私は最近Next.jsを使い始めたエンジニアです。
普段はSESとして業務にあたっていますが、現場では既に決まった設計に沿って実装することが多く、自分で設計判断を下す機会がなかなかありません。
「ただ動くものを作るだけでなく、保守性やテストのしやすさにこだわった一歩先の開発を自力でできるようになりたい」
そんな思いから、今回のポートフォリオ開発では、依存性の逆転 (DIP) を重視した「クリーンアーキテクチャの考え方を取り入れたレイヤード構成」を採用しました。この記事は、初心者が設計に苦悩しながらたどり着いた構成と、その過程での学びをまとめた備忘録です。
本記事のスタンス
書籍通りの厳密な定義の再現よりも、Next.jsの個人開発において「依存関係を整理して楽に開発する」ことに主眼を置いています。そのため、一部は実践しやすさを優先した構成になっています。
背景:なぜNext.jsに設計が必要だったか
App Routerで開発を始めた当初は、Server Actionsの中にバリデーションやDB操作などのロジックをすべて直接記述していました。しかし、機能が増えるにつれ、以下のような課題に直面しました。
- テストが書きにくい: Server ActionsはNext.jsランタイムに依存するため、純粋なロジックだけの単体テストを切り出すのが困難でした
- UIとの密結合: UIの都合とビジネスルールが混ざり合い、コードを変更した際の影響範囲が読みづらくなっていきました
- 将来への不安: 今後「ゲストログイン」や「複雑な権限管理」を追加したとき、コードがスパゲッティ化する未来を容易に想像できてしまいました
これらを解決し、「UI」「フレームワーク」「ビジネスロジック」を明確に切り離すために、クリーンアーキテクチャの思想を導入することを決意しました。
クリーンアーキテクチャの思想とは
クリーンアーキテクチャの真髄は**「依存性の逆転 (DIP)」**です。
依存性の方向を制御し、システムの根幹になるドメインルールを守ることにあります。
私は今回、抽象度を上げてざっくり「内側」と「外側」で定義しました。
- 中心(Domain / UseCase): 「BANされたユーザーはログインできない」など、仕組みそのものがサービス提供に直結する不変のビジネスルール
- 外側(Infrastructure / UI): Next.js、Supabase、shadcn/uiといった、いつでも差し替え可能な具体的な「道具」
各階層の役割について
Domain (Entities)
システムが扱う「概念」と、それが守るべき「ルール」を定義します。単なる型定義の置き場ではなく、ビジネスルールを型とロジックで表現する場所です。
| 分類 | 置く場所 | 内容・具体例 |
|---|---|---|
| エンティティ | domain/entities/ |
単一のオブジェクトが守るべき制約 例:パスワードのバリデーション、権限チェック。 |
| リポジトリ(IF) | domain/repositories/ |
データの出し入れに関する「契約(interface)」 「保存する」という行為の定義のみを置きます。 |
UseCase
ユーザーが「やりたいこと」を実現する手順書です。Domainで定義したインターフェースを使い、具体的な実装(Supabase等)を知らないままロジックを組み立てます。
Infrastructure (Interface Adapters)
DB操作(Supabase/Prisma)などの具体的な実装を行います。ここはDomain層のインターフェースに依存します。
Presentation (UI / Framework)
Server Actionsは「HTTPリクエストの入り口(コントローラー)」と割り切り、依存関係の組み立て(手動DI)とユースケースの呼び出しだけに専念させます。
UI部品とNext.jsの仕組みを混ぜない
app/ ディレクトリはNext.jsの進化に影響を受けやすい場所です。一方で、presentation/components/ に置く部品は、より純粋なReactコンポーネントとしての責務に集中させます。ディレクトリレベルで分けることで、大規模なアップデートに強いコードになると考えました。
実際に導入したディレクトリ構成
src/
├── app/ # Next.js App Router (page.tsx, actions.ts)
├── domain/ # 【最内部】不変のビジネスルール
│ ├── entities/ # ルールを表現する型 + ロジック
│ └── repositories/ # インターフェース(契約)
├── usecase/ # 【内部】アプリケーションの手順書
├── infrastructure/ # 【外部】具体的な実装(Supabase等)
└── presentation/ # 【外部】UIパーツ(純粋なReact部品)
└── components/ # UI部品
導入してみて感じたメリット
テストコードが短くて書きやすい
ドメインロジックがNext.jsから完全に独立しているため、DBやネットワークを模倣(モック)することなく、純粋な関数としてテストが書けるようになりました。
1ファイルでやるべき責務が限られているから認知負荷がかかりにくい
「このファイルはバリデーションだけ」「このファイルは手順だけ」と役割が分かれているため、コードを読むときに集中すべきポイントが明確になりました。
隙間時間で作業しやすい
「今日はドメインのルール定義だけ」「明日はリポジトリの実装だけ」と作業を細分化できるため、まとまった時間が取れない個人開発でも着実に進捗を出せました。
生成AIが出力するコードの品質向上
これが最大の発見でした。ファイルの責務が明確なため、プロンプトに渡すコンテキストが整理され、生成AIが迷いにくくなりました。また、コードが小さく分割されているため人間側のレビューコストも下がり、設計を整えることは、AIへの最高のプロンプトになるのだと実感しています。
導入してみて感じたデメリット
コード量が増える
シンプルな処理でも複数層を経由するため、記述量は増えます。しかし、ルールが定まっていれば生成AIがコードを記述しやすくなるため、現代の開発スタイルでは解決しやすいデメリットだと感じています。
学習コストがかかる
「これはDomainか?UseCaseか?」という判断に最初は時間がかかります。正解がない中で自分なりの基準を作るまでが大変でしたが、この試行錯誤こそが「設計の筋肉」を鍛えてくれていると感じています。
ボイラープレートが増える
DIのための記述など、設計を維持するための「儀式」は増えます。これもパターンが決まっているため、生成AIを活用することで本質的な「設計判断」に集中できるようになりました。
おわりに
最初は個人開発にオーバーワークかと思いましたが、AIとの相性の良さや、隙間時間での開発のしやすさなど、予想外のメリットがありました。
まだまだ勉強中ですが、SESの現場ではなかなか経験できない「設計の責任を自分で持つ」という体験を通じて、少しずつエンジニアとしての視座が上がっている気がします。
正直、色々調べながら進めている最中なので、まだ完全に理解しきれていない箇所や改善の余地もあると思います。
これからも実際に手を動かしながら、より良い設計を目指して改良を重ねていきたいと思います!