【Node.js】サーバーサイドディレクトリ構成について可能な範囲でクリーンアーキテクチャっぽくしてみた

株式会社Another works CTOの塩原です。
弊社では、現在Node.jsを使ったgRPCサーバーを構築しています。

その中で、現段階でのサーバーサイドアプリケーションのディレクトリ構成を公開したいと思います！

# 環境
- gRPC
- Node.js　version 14系
- Typescript



# 主なディレクトリー構成図
弊社ではmonorepoを採用しているので、.protoファイルを別ディレクトリーで管理しています。
層としてはapplication, domain, infraの三層構造となっています。

```
PROJECT
  |- grpc_proto/
  |- project_a/
　　　　　　　　|- gen/
      - XXX_pb.d.ts
      - XXX_grpc_pb.js
      - XXX_pb.js
    |-src/
      |- application/
        |- controller/
        |- converter/
        |- grpc/
        |- helper/
      |- consts/　# 定数ファイルをまとめたディレクトリ。どの層からも使うことができる
      |- domain/
        |- converter/
        |- entity/
        |- model/
        |- repository/
        |- service/ 
      |- infra/
        |- s3/
          |- datasource/
          |- entity/
　　　　　　　　　　　　　　　　　　　　|- S3Client.ts     
```

# Application層
主に、client側との接点を持つ層。
protoで生成されたクラスはこの層だけが使うことができる

## grpc
エンドポイントごとに作成。
serviceを読んだりする処理などは、controller層で行う。
このディレクトリでは、リクエストを受け取って、レスポンスを返すといった薄い役割のみにしている

## controller
grpcのエンドポイントと一対一でクラスを実装する。
serviceクラスを複数実行したり、domainクラスをレスポンス用の型に変換するために、converterを実行したりする。
domain層との接点はこの層が持つ。

# Domain層
ビジネスロジックを担当。この層では、grpcの自動生成されたコードをこの層で使うことはできない
## entity
serviceクラスのメソッドに対する引数で複雑なものはこちらで定義する
クラスは使わずinterfaceのみとなるので、typesでもいいかもしれない

## model
いわゆるドメインモデル
クラスで実装する
サーバーサイドだと、データベースのテーブル型をそのままドメインモデルとすることが多いが(Railsなど)、後述するinfra層のデータアクセス先が増えることを考えて分離した

## repository
serviceクラスから使われる。
infra層のdatasourceなどを複数組み合わせて使う。
domainモデルと一対一の関係となっていて、datasourceから返ってくるレスポンスをconverterを使ってdomainモデルに変換して、返す。

## service
ビジネスロジックを実装するディレクトリ。
一つのクラスにつき、一つのメソッドとしている
serviceの下にドメインモデルごとにディレクトリーを切る。
一つのサービスが複数のドメインを跨ぐことはあり得るので、その場合は主になるドメインのディレクトリの中に実装する
userを作成する処理の時にuser_profileというドメインモデルがあるとき同じサービスクラス内で処理する場合がある。その場合はuserが主なのでuser/以下にサービスクラスを実装する

```
domian
  |- service
    |- user
      |- UserUpdateService.ts
　　　　　　　　　　　　|- UserCreateService.ts
```

```typescript
export default class UserUpdateService {
  constructor() {}
  call() {
    // 具体的な処理
  }
}
```

# infra層
外部からデータを取ってくる処理はすべてこちらでwrapする
今回はS3のみだが、Redis, MySQL, 別のマイクロサービスなどなどからデータを取得する時もすべてこの層でwrapする。
データ取得先ごとにディレクトリを切り、その中にそれぞれ、datasourceとentityを作る

```
infra
  |- redis
    |- datasource
      |- UserRankingRedisDataSource.ts
    |- entity
      |- UserRankingEntity.ts
    RedisClient.ts
  |- s3
    |- datasource
      |- UserS3Datasource.ts
      |- MetaTagS3DataSource.ts 
    |- entity
      |- UserEntity.ts
      |- MetaTagEntity.ts
    S3Client.ts
```

## entity
データ取得先のレスポンスをinterfaceで定義したもの。
またdatasourceに対して、渡すパラメーターなどをinterfaceで定義することもある。
こちらもtypeでもいいかなとは思ってる

## datasource
外部からの取得処理を書く。
s3であれば、ある程度のファイルの種類ごとに作ったりする。
databaseから取得する際はこの中でSQLを書いたりするような処理を書く。

# データフロー
![スクリーンショット 2021-08-26 8.33.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114212/e046306f-f226-3777-8ed2-fb517c9aef0e.png)

## controller -> service
controller層ではprotoから自動で生成されたモデルを扱う
serviceクラスを呼び出す際に、　複雑なデータであればconverterを使って、domain側のentityに変換して渡してあげる
serviceからはdomainモデルで返ってくるので、それをapplication側のconverterを使って、protoから自動生成されたモデルに変換して、grpc層に返す

## repository -> datasource
datasourceを実行した時の返り値として、infra層のentityが返ってくるため、converterを使ってdomainモデルに変換をおこなう。

# まとめ

今回意識したポイントとして、外部の層と接点を持つ役割を各層一つにするということを意識しました。
application層ならcontroller, domain層ならrepositoryみたいな
converterをいろんなところから呼ばれるとデータ変換がいろんなところで行われてしまうので、それを避けるために、ルートにおかず、層の中に閉じ込めました。

まだまだ、設計で気になる部分はあるので、逐次アップデートを続けていきます。

▼複業でスキルを活かしてみませんか？複業クラウドの登録はこちら！
https://talent.aw-anotherworks.com/?login_type=none