🧇

NestJSにおけるクリーンアーキテクチャなディレクトリ構造

2022/03/14に公開

追記: こちらの記事も書きました!
https://zenn.dev/trape_inc/articles/bfc23c829c2eb2

ディレクトリ構造いろいろ

ディレクトリ構造って結構悩みますよね。NestJSに限らずいくつかパターンを上げてみたいと思います。

Rails型

僕が最初に学んだディレクトリ構造です。

├── controllers
│   ├── userController.ts
│   └── chatController.ts
├── services
│   ├── userService.ts
│   └── chatService.ts
├── models
│   ├── user.ts
│   └── chat.ts
└── configs

Rails型クリーンアーキテクチャ

僕がクリーンアーキテクチャを学んで作ったディレクトリ構造です。

├── presentations
│   ├── userController.ts
│   └── chatController.ts
├── usecases
│   ├── userService.ts
│   └── chatService.ts
├── domains
│   ├── user
│   │   ├── userRepositoryInterface.ts
│   │   └── userEntity.ts
│   └── chat
│       ├── chatRepositoryInterface.ts
│       └── chatEntity.ts
├── infrastructures
│   ├── sequelize
│   │   └── userRepository.ts
│   └── dynamodb
│       └── chatRepository.ts
└── configs

主要ドメイン分割クリーンアーキテクチャ

Rails型のクリーンアーキテクチャを継承しつつ、主要ドメインで最初のディレクトリを分けた構造です。どっかの会社が採用しているのをどこかの記事で見ました。主要ドメインディレクトリ配下に関連ドメインが収まります。例えば、チャットドメイン配下に参加者リストドメインが入ってくるみたいな感じです。だから主要ドメイン本体をcoreとして分離させています。

├── chat
│   ├── presentations
│   │   └── chatController.ts
│   ├── usecases
│   │   └── chatService.ts
│   ├── domains
│   │   └── core
│   │       ├── chatRepositoryInterface.ts
│   │       └── chatEntity.ts
│   ├── infrastructures
│   │   └── dynamodb
│   │       └── chatRepository.ts
│   └── configs
└── account
    ├── presentations
    │   └── userController.ts
    ├── usecases
    │   └── userService.ts
    ├── domains
    │   └── core
    │       ├── userRepositoryInterface.ts
    │       └── userEntity.ts
    ├── infrastructures
    │   └── sequelize
    │       └── userRepository.ts
    └── configs

モジュール型クリーンアーキテクチャ

nestjsでクリーンアーキテクチャやるならこの構造になると思います。フォルダ名はなんとなくで命名しているので、entityinfraなどは好きな名前でもいいと思います。

├── chat
│   ├── core
│   │   ├── entity
│   │   │   ├── chat.entity.ts
│   │   │   └── chat.repository.interface.ts
│   │   ├── infra
│   │   │   └── chat.repository.dynamodb.ts
│   │   ├── chat.controller.ts
│   │   ├── chat.service.ts
│   │   └── chat.module.ts
│   └── chat.index.module.ts
└── account
    ├── core
    │   ├── entity
    │   │   ├── user.entity.ts
    │   │   └── user.repository.interface.ts
    │   ├── infra
    │   │   └── user.repository.sequelize.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
    │   └── user.module.ts
    └── account.index.module.ts

クエリサービスを導入したい

モジュールディレクトリに直接user.query.service.interface.tsとインターフェースを起きます。次にinfraディレクリに実際の実装user.repository.sequelize.tsを起きます。

└── account
    ├── core
    │   ├── entity
    │   │   ├── user.entity.ts
    │   │   └── user.repository.interface.ts
    │   ├── infra
    │   │   ├── user.query.service.sequelize.ts
+   │   │   └── user.repository.sequelize.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
+   │   ├── user.query.service.interface.ts
    │   └── user.module.ts
    └── account.index.module.ts

外部ライブラリをモジュールとして導入したい

nestjsのやり方の従うと、sequelizeやprismaなど外部ライブラリは直接importせずに、モジュール管理して、使いたいサービスにDIすることになります。作成の仕方に2パターンあります。

パターン1 ライブラリの固有名詞を隠す。

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
└── cache
    ├── infra
    │   └── cache.service.redis.ts
    ├── cache.service.interface.ts
    └── cache.module.ts

パターン2 ライブラリの固有名詞を使う。

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
└── sequelize
    ├── sequelize.service.ts
    └── sequelize.module.ts

パターン1は、redisのもつキャッシュという役割の前面にだし、抽象化しています。そのため、cache.service.tsではなくてcache.service.interface.tsになっています。実装はinfraディレクトリで行っています。

パターン2は、sequelizeそのものをサービス化しています。

抽象化の観点からはパターン2よりパターン1のほうがおすすめです。パターン2は、主に他のディレクトリのinfraディレクトリ内で共通で使うライブラリを注入するために利用します。

抽象化に関してはこちらの記事で詳しく解説しています。
https://zenn.dev/dove/articles/40ec3779b39ac6

ドメインイベント関連のディレクトリが入ると

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── event
│   │   │   └── account.created.event.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
├── notification
│   ├── listener
│   │   └── account.created.listener.ts
│   ├── notification.service.ts
│   └── notification.module.ts
└── share
    ├── baseEvent.ts
    └── util.ts

実装はこちらを参照してください。
https://zenn.dev/dove/articles/40476144e8bf9a

Discussion