📑

NestJS・prismaでクリーンアーキテクチャを参考に構成を考える

2023/11/05に公開

概要

こんにちは。バックエンドエンジニアのfuです。
私はTypeScriptが好きでバックエンドはTS・NestJSで構築することが多いです。(フロントとバックをTSで揃えたら時のメリットデメリットはこちら

FWとしてNestJSを選定した場合、moduleパターンという決まりはありますが割と自由度が高くちゃんとアーキテクチャを考える必要があります。

そこで、ORMにPrismaを採用し、クリーンアーキテクチャを参考にしてこんなアーキテクチャが開発しやすいんじゃないかと考えているものを記載していこうと思います。

※ここではNestJS、Prisma、クリーンアーキテクチャについての詳細な説明は行いませんのであらかじめご了承ください🙇‍♂️

基本方針

SOLID原則をできるだけ守り、クリーンアーキテクチャを取り入れた構成とする。

SOLID原則

  • S:SRP、単一責任の原則(オブジェクトは1つの責任のみを果たすように設計するべき)
  • O:OCP、開放閉鎖の原則(ソフトウェアに新しく機能を追加するとき、既存のコードを変更せず新しいコードを追加するだけで済むようにしておくべき)
  • L:LSP、リスコフの置換原則(サブタイプのオブジェクトはスーパータイプのオブジェクトの仕様に従わなければならない)
  • I:ISP、インタフェース分離の原則(分割できるインターフェイスは分割するべき)
  • D:DIP、依存性逆転の原則(プログラムの重要な部分が、重要でない部分に依存しないよう設計すべき)

を主軸に考える。

※今回紹介するフォルダ構成についてはSOLID原則は関係ないところもあるかもしれないが、根底にあるのはこの考え方という意味で記載しておく。

バリデーションについて

  • prismaはDBから取得してきたデータをEntityではなくObjectとして扱うのが主流のため、モデルにメソッドを生やしたりバリデーションを行うといったことはやりにくい。
  • バリデーションについては、class-validatorを活用してDTOにバリデーション内容を記述し、フロントからリクエストをもらった時にバリデーションを行うこととする。

ファイルの取り扱い

  • クリーンアーキテクチャに基づき、基本的に1ファイル1機能とするため、1ファイルでexportするものは一つとする。
  • 型を定義するファイルは、ある機能を実装するために1ファイル複数exportを行ったほうが管理しやすい場合は例外とする。

補足

  • ORMにはPrismaを用いており、GraphQLの場合はPrismaから生成されるinput、output、enumファイルなどを使用することで開発効率・メンテナンス性を上げることができる(ここでは詳細な説明を省く)。
  • クリーンアーキテクチャで言うところのPresenterやViewModel、UseCaseごとにDTOを定義するという考え方は私の経験してきた小〜中規模のプロダクトではメリットよりデメリットが大きいと判断したためここでは使用しない。

フォルダ構成

RestAPIを想定したフォルダ構成を下記に記す。

src
├── application            Application Business Rules  ソフトウェアが何ができるのかを表現
│   └── use-cases          インターフェースで定義したメソッドから呼ばれ、何を実現したいかのユースケースを表現
├── domain                 Enterprise Business Rules  ビジネスロジックを表現するための機能が集まる
│   ├── repositories       データベースアクセスに関する処理を行い、use-cases・servicesから呼ばれる
│   └── services           ドメインサービス。ユースケースに乗せるべきではないビジネスロジック部分を担保
├── infrastructure         Frameworks & Drivers。ビジネスロジック以外のもの
│   ├── decorators         Controllerで使用するデコレータ
│   ├── filters            エラーのフィルタリング処理
│   ├── guards             ログイン関係のガード
│   ├── ioc                各種module
│   ├── metadata           権限などに関するメタデータを定義
│   ├── middleware         クエリ発行後に呼ばれるミドルウェア
│   ├── plugins            ログ出力などのプラグイン
│   ├── prisma             Prismaにより出力されたファイルやFactory・seed関連
│   │   ├── factories      テストやseedで事前データを作成する際に使用するfactory
│   │   ├── migrations     マイグレーションファイル
│   │   ├── seed.ts
│   │   └── schema.prisma
│   └── strategies         guardsから呼び出されるストラテジー
├── interfaces             Interface Adapters。入力、永続化、表示を担当するオブジェクトが所属
│   ├── batches            バッチ処理の入り口を定義
│   ├── controllers        frontendから呼び出されるコントローラーを定義
│   └── dto                外部と接続するためのinput、output
├── types                  複数箇所で呼び出す型を定義
├── utils                  いろいろな箇所で呼ばれる便利なもの
│   ├── functions          共通関数を定義
│   ├── statics            定数を定義
│   └── test-helper        テストで呼ばれるヘルパーを定義
└── main.ts                エントリーポイント

主要フォルダの詳細設計

application

  • use-cases: controllersまたはbatchesで定義したクラスから呼び出される。
    • domain/servicesやrepositoriesをここから呼び出しデータを加工して返却。
    • ユースケースはAPI単位で存在し、1ファイル1メソッドとする。
    • ここで定義するのはあくまで〜ができるというユースケースの説明なので、細かいビジネスロジックは持たない。

domain

  • repositories: DBアクセスに関する処理を記述。
    • テーブル単位で1ファイルに各種処理を記述する。
    • repositoriesについてはdomain/repositoriesで抽象Classを定義し、interfaces/repositoriesで「.impl」ファイルに実態を持たせて依存性逆転の原則を守ることもできるが、型が扱いにくくなることや初学者に理解してもらうのにハードルが高かったことからやめた。
  • services: ビジネスロジックを定義。use-cases・repositoriesで事足りる場合は無理に定義しない。servicesからrepositoriesを呼び出すことはできるが、依存の方向を一定にするため逆は不可。

infrastructure

  • 上位のレイヤを支える一般的な技術的機能を提供する。
  • 認証周り(ガード、ストラテジー)やDB関連の仕組みに加え、ミドルウェア・フィルター等を含む。
  • ビジネスロジックに直接関わらない部分をここで定義しているが、ちょっと詰め込み過ぎな気がするので他のフォルダを作って管理すべきものもありそう。

interfaces

  • batches: ECSタスクスケジューリングなどのバッチ処理やNestJSのスケジュール実行で使用される想定。
  • controllers: フロントエンドからリクエストを受ける部分。
  • 認証認可部分をデコレータで定義し、基本的にはユースケースを呼び出すだけ。
  • トークンをCookieに詰めるなど、アプリケーション外のものと接続するような処理はコントローラーに記述することもある。

テスト設計・方針

  • テスト対象は以下のフォルダ構成に記載されたファイルとし、同じ階層に~spec.tsを配置する
- src/
  - domain/
    - services/ 
    - repositories/
  - interfaces/
    - batches/
    - controllers/
    - dto/
  - utils/
    - functions/
  • domain/repositoriesのテストでDB関連処理、domain/servicesでビジネスロジック関連処理のテストを行う(unit test)
  • dtoではバリデーションテストを行う
  • controllersのテストをintegration testと位置づけ、use-caseのテストについてはこちらのintegration testで賄う
    • 権限による認可のテスト
    • ステータスコード・返却される値が期待するものなのかの確認
    • エラーが想定されるような処理の場合、エラーレスポンスの確認

終わりに

ここまで読んで頂きありがとうございました。
NestJS・prismaで開発しやすいんじゃないかなと私が考えているアーキテクチャの説明は以上になります。

自分の中でも試行錯誤を行なってこの構成に辿り着きましたが、まだまだ改善の余地はあると思っているので何かご意見等ございましたらコメント頂けると幸いです🙇‍♂️

Discussion