【NestJS】API開発初心者がDDD勉強してみたいのでオニオンアーキテクチャやってみた

## はじめに

お疲れ様です。
[ふぁる](https://twitter.com/fal_engineer)です。
趣味でフロントエンドを勉強しています。
本業では**オンラインMTGのコメント賑やかし部隊**として、日々冷やかしに励んでおります。（雇用業種はサクラです。給与はどんぐりです）

フロントばかり触っていては幅が狭まっちゃうかと思い、個人開発でDDDっぽいやつやってみました。
リポジトリはPublicにしていますし、採用した各実装パターンについても掘り下げてみますので、ぜひ読んでってください。

うぇ～い（この先出てくる単語すべてが仰々しいので中和のための一文）

## 技術構成
### 実装パターン・アーキテクチャ

- ドメイン駆動開発（DDD）
    - 言及しません。
- オニオンアーキテクチャ
    - 層の責務が明確であるのと、名著「[ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本](https://www.amazon.co.jp/%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E5%85%A5%E9%96%80-%E3%83%9C%E3%83%88%E3%83%A0%E3%82%A2%E3%83%83%E3%83%97%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B-%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E3%81%AE%E5%9F%BA%E6%9C%AC-%E6%88%90%E7%80%AC-%E5%85%81%E5%AE%A3/dp/479815072X)」で学んだ内容と比較し、各層の名称付けや責務が直感的に被る部分が多く感じました。学習がてら実装を進めるには良いのではないかと採用しました
    - 玉ねぎ好きなんですよ
- IoCパターン
    - 依存性逆転の法則
    - **ハリーポッター読むみたいなテンションで公式ドキュメント読んで、「インセンディオ！🔥」ってユニバで買った杖振り回すみたいなノリで実装進めました**
    - ユニットテストも現在（2023/09/27）実装出来ていないため、現時点では複雑度を増すばかりで旨味に与れていません。旨味に与りたいので、ユニットテストは書きます。コッカラッス
- DTOパターン
    - POST,PUTリクエストのbodyのバリデーション
    - レイヤー間のデータ転送を効率的に行うため採用しました

### ライブラリ

- TypeORM
    - なんかどれ参考にしてもこの人使ってたので、そんなにいいものなのかなぁと採用しました
    - prismaよりはつらくなかったですがちゃんとつらかったです。
- Swagger
    - `@nestjs/swagger`
    - ドキュメントとして見れるのオシャレだし......
- JWT
    - `@nestjs/passport`、`passport-jwt`あたり
    - 大分変わった構成だと思いますが、
    ｟twitter認証 → jwtトークンを発行し、フロントエンドとの認証認可｠
    みたいにしているため、JWTの発行・管理を行っています。
- eslint, prettier
    - とにかく動かしまくって理解したく、開発スピードを超担保したかったので、Huskyは最初入れませんでした
    - 普通に最初からHusky入れとけば良かったなー。

### 外部サービス

- Heroku
    - ホスティングに用いています。→ `./Procfile`
- Twitter（**x** ...?）
    - ログインは現状、Twitterアカウントを用いたもののみです。

## オニオンアーキテクチャとは

↓ よく見るあれ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/26d6ad80-6b1b-a312-2f73-cad904b7cd41.png)

直感的にこちらが、すごくわかりやすかったです。
[DDDの一般的なアーキテクチャをまとめてみた](https://zenn.dev/ayumukob/articles/ff183004d09ede#%E3%82%AA%E3%83%8B%E3%82%AA%E3%83%B3%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3)

全ての依存が中心の層に向かい、逆に中心から外の層へは依存しません。
しっかり書くとボロが出そうなのと、書ききれる自信がないので、一旦この程度に留めます。

## 作ったもの

都々逸の投稿サイト作りました。

https://dodoitsu-life.vercel.app/dodoitsu/detail/412321f1-d6ed-4a24-b0c0-2da229138352

リポジトリです。

https://github.com/dodoitsu-life/dodoitsu-life-web-api

ついでにフロントのリポジトリです。
今度記事出しますが、Next.jsのAppRouter産です。

https://github.com/dodoitsu-life/dodoitsu-life-front

## コード

<details><summary>全体的なディレクトリ構造</summary>

```md
.
├── .eslintrc.js
├── .prettierrc
├── .vscode
│   └── settings.json
├── Dockerfile.dev
├── Procfile
├── README.md
├── docker-compose.dev.yaml
├── env
│   ├── .env.docker
│   └── .env.local
├── nest-cli.json
├── package.json
├── src
│   ├── app.module.ts
│   ├── application
│   │   ├── auth
│   │   │   ├── auth.controller.ts
│   │   │   ├── auth.module.ts
│   │   │   ├── dto
│   │   │   │   └── token-refresh.dto.ts
│   │   │   └── guards
│   │   │       └── optional-jwt-auth.guard.ts
│   │   ├── dodoitsu
│   │   │   ├── dodoitsu.controller.ts
│   │   │   ├── dodoitsu.module.ts
│   │   │   ├── dodoitsu.service.ts
│   │   │   └── dto
│   │   │       ├── create-dodoitsu.dto.ts
│   │   │       ├── response-dodoitsu-like.dto.ts
│   │   │       └── response-dodoitsu.dto.ts
│   │   └── user
│   │       ├── dto
│   │       │   └── response-user.dto.ts
│   │       ├── user.controller.ts
│   │       ├── user.module.ts
│   │       └── user.service.ts
│   ├── common
│   │   ├── ApiResponse.ts
│   │   ├── code
│   │   │   └── Code.ts
│   │   ├── exception-filter
│   │   │   └── ExceptionFilter.ts
│   │   └── type
│   │       └── Types.ts
│   ├── config
│   │   ├── app.config.ts
│   │   └── data-source.ts
│   ├── domain
│   │   ├── auth
│   │   │   ├── auth.repository.interface.ts
│   │   │   └── auth.service.ts
│   │   ├── dodoitsu
│   │   │   ├── dodoitsu-like.entity.ts
│   │   │   ├── dodoitsu.entity.ts
│   │   │   ├── dodoitsu.repository.interface.ts
│   │   │   └── dodoitsu.service.ts
│   │   └── user
│   │       ├── user.entity.ts
│   │       ├── user.repository.interface.ts
│   │       └── user.service.ts
│   ├── infrastructure
│   │   ├── auth
│   │   │   ├── jwt.strategy.ts
│   │   │   ├── optional-jwt.strategy.ts
│   │   │   └── twitter.strategy.ts
│   │   ├── orm
│   │   │   ├── dodoitsu
│   │   │   │   └── dodoitsu.repository.ts
│   │   │   ├── migrations
│   │   │   │   └── ...
│   │   │   ├── typeorm-util.ts
│   │   │   └── user
│   │   │       └── user.repository.ts
│   │   └── orm
│   └── main.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
```
</details>

### 各レイヤーについて
この章では、各レイヤーの実装に触れつつ、責務を書きます。

#### ドメイン層

扱うドメインに関連するビジネスロジックを処理します。
例に取り、`dodoitsu`のコードを見ていきます。

##### エンティティ
<details><summary>dodoitsu.entity.ts</summary>

```ts
import {
  Entity,
  PrimaryColumn,
  Column,
  OneToMany,
  ManyToOne,
  JoinColumn,
} from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { DodoitsuLike } from '@domain/dodoitsu/dodoitsu-like.entity';
import { User } from '@domain/user/user.entity';

@Entity()
export class Dodoitsu {
  @PrimaryColumn('uuid')
  id: string = uuidv4();

  @Column()
  content: string;

  @Column({ nullable: true })
  description: string;

  @ManyToOne(() => User, { nullable: true })
  @JoinColumn({ name: 'authorId' })
  author?: User;

  @OneToMany(() => DodoitsuLike, (like) => like.dodoitsu)
  likes?: DodoitsuLike[];

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt;
}
```
</details>

エンティティは、データベースのテーブルとその構造を表現するクラスです。
`@Entity`をデコレータをつける事で、Typeormがこのクラスをデータベースのテーブルとして扱ってくれます。

##### リポジトリの抽象

<details><summary>dodoitsu.repository.interface.ts</summary>

```ts
import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';
import { CreateDodoitsuDto } from '@application/dodoitsu/dto/create-dodoitsu.dto';

import { User } from '@domain/user/user.entity';

export interface FindOptions {
  skip?: number;
  take?: number;
  order?: {
    [P in keyof Dodoitsu]?: 'ASC' | 'DESC';
  };
}

export interface IDodoitsuRepository {
  find(options: FindOptions): Promise<Dodoitsu[]>;
  findOne(id: string): Promise<Dodoitsu | null>;
  findWithLikesOrder(options: FindOptions): Promise<Dodoitsu[]>;
  count(): Promise<number>;
  findByUser(options: FindOptions, userId: string): Promise<Dodoitsu[]>;
  countByUser(userId: string): Promise<number>;
  findLikedByUser(options: FindOptions, userId: string): Promise<Dodoitsu[]>;
  countLikedByUser(userId: string): Promise<number>;
  create(dodoitsu: CreateDodoitsuDto, author?: User): Promise<Dodoitsu>;
  save(dodoitsu: Dodoitsu): Promise<Dodoitsu>;
  like(dodoitsu: Dodoitsu, user: User): void;
  unlike(dodoitsu: Dodoitsu, user: User): void;
  didUserLike(dodoitsuId: string, userId: string): Promise<boolean>;
}
export const SYMBOL = Symbol('IDodoitsuRepository');
```
</details>

Dodoitsuエンティティに関連するリポジトリのインターフェースを定義しています。
データの取得や保存など、データベースとのあれやこれやを抽象化するものです。

ソート条件、ページングを`FindOptions`として型定義し、汎用的にしています。
リポジトリで返却するのはエンティティです。

*SYMBOL 定数
... NestJSにおける依存性注入で、リポジトリを識別するために必要となるプロバイダトークンです。
定数として定義しておいて、呼び出せば済む形で扱いやすくしています。

##### ドメインサービス

<details><summary>dodoitsu.service.ts</summary>

```ts
import { Inject, Injectable, NotFoundException } from '@nestjs/common';

import {
  IDodoitsuRepository,
  SYMBOL,
} from '@domain/dodoitsu/dodoitsu.repository.interface';
import { CreateDodoitsuDto } from '@application/dodoitsu/dto/create-dodoitsu.dto';
import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';

import { User } from '@domain/user/user.entity';

@Injectable()
export class DodoitsuService {
  constructor(
    @Inject(SYMBOL)
    private readonly dodoitsuRepository: IDodoitsuRepository,
  ) {}

  async countAll(): Promise<number> {
    return this.dodoitsuRepository.count();
  }

  async findLatest(page: number, limit: number): Promise<Dodoitsu[]> {
    return this.findDodoitsuByOrder(page, limit, { createdAt: 'DESC' });
  }

  async findPopular(page: number, limit: number): Promise<Dodoitsu[]> {
    const totalCount = await this.dodoitsuRepository.count();
    const totalPages = Math.ceil(totalCount / limit);

    if (page > totalPages) {
      throw new NotFoundException(
        `Invalid page number. There are only ${totalPages} pages.`,
      );
    }
    return this.dodoitsuRepository.findWithLikesOrder({
      order: {
        likes: 'DESC',
      },
      take: limit,
      skip: (page - 1) * limit,
    });
  }

  async findOne(id: string): Promise<Dodoitsu | null> {
    const dodoitsu = await this.dodoitsuRepository.findOne(id);
    if (!dodoitsu) {
      throw new NotFoundException(`Dodoitsu with ID ${id} not found`);
    }
    return dodoitsu;
  }

  async create(dto: CreateDodoitsuDto, user?: User): Promise<Dodoitsu> {
    const dodoitsu = await this.dodoitsuRepository.create(dto, user);
    return this.dodoitsuRepository.save(dodoitsu);
  }

  async likeDodoitsu(dodoitsu: Dodoitsu, user: User): Promise<void> {
    return this.dodoitsuRepository.like(dodoitsu, user);
  }

  async unlikeDodoitsu(dodoitsu: Dodoitsu, user: User): Promise<void> {
    return this.dodoitsuRepository.unlike(dodoitsu, user);
  }

  async didUserLike(dodoitsuId: string, userId: string): Promise<boolean> {
    return this.dodoitsuRepository.didUserLike(dodoitsuId, userId);
  }

  async findByUserId(
    userId: string,
    page: number,
    limit: number,
  ): Promise<Dodoitsu[]> {
    return this.dodoitsuRepository.findByUser(
      {
        take: limit,
        skip: (page - 1) * limit,
        order: { createdAt: 'DESC' },
      },
      userId,
    );
  }

  async countByUserId(userId: string): Promise<number> {
    return this.dodoitsuRepository.countByUser(userId);
  }

  async findLikedByUserId(
    userId: string,
    page: number,
    limit: number,
  ): Promise<Dodoitsu[]> {
    return this.dodoitsuRepository.findLikedByUser(
      {
        take: limit,
        skip: (page - 1) * limit,
      },
      userId,
    );
  }

  async countLikedByUserId(userId: string): Promise<number> {
    return this.dodoitsuRepository.countLikedByUser(userId);
  }

  private async findDodoitsuByOrder(
    page: number,
    limit: number,
    order: { [P in keyof Dodoitsu]?: 'ASC' | 'DESC' },
  ): Promise<Dodoitsu[]> {
    const totalCount = await this.dodoitsuRepository.count();
    const totalPages = Math.ceil(totalCount / limit);

    if (page > totalPages) {
      throw new NotFoundException(
        `Invalid page number. There are only ${totalPages} pages.`,
      );
    }

    return this.dodoitsuRepository.find({
      order,
      take: limit,
      skip: (page - 1) * limit,
    });
  }
}
```
</details>

ドメインサービスでは、ビジネスロジックやルールをカプセル化します。
具体的には、DB等の永続化層へのアクセスを抽象化し、直接のデータベース操作を隠蔽します。
これにより、ビジネスロジックがクリーンな状態を保ち、アプリケーションの柔軟性とメンテナンス性が向上します。

まず、IoCパターンを導入した場合の特徴として、リポジトリの実体を呼び出しません。
以下です。

```ts
// リポジトリの実体は直接呼び出さず、抽象のみをimportする
import {
  IDodoitsuRepository,
  SYMBOL,
} from '@domain/dodoitsu/dodoitsu.repository.interface';

// 注入可能な事を示すデコレータ
@Injectable()
export class DodoitsuService {
  constructor(
    // トークンベースでIDodoitsuRepository型のリポジトリを注入可能にする
    @Inject(SYMBOL)
    private readonly dodoitsuRepository: IDodoitsuRepository,
  ) {}
```

ビジネスロジックのカプセル化という点では、以下のあたりがわかりやすそうです。

```ts
  // 最新の投稿を取得する
  async findLatest(page: number, limit: number): Promise<Dodoitsu[]> {
    return this.findDodoitsuByOrder(page, limit, { createdAt: 'DESC' });
  }

  // repositoryに渡しやすい形にデータを整形、エラーをthrowする便利関数
  private async findDodoitsuByOrder(
    page: number,
    limit: number,
    order: { [P in keyof Dodoitsu]?: 'ASC' | 'DESC' },
  ): Promise<Dodoitsu[]> {
    const totalCount = await this.dodoitsuRepository.count();
    const totalPages = Math.ceil(totalCount / limit);

    // throwしたエラーはcommonのExceptionFilterがキャッチしレスポンス型に成形する
    if (page > totalPages) {
      throw new NotFoundException(
        `Invalid page number. There are only ${totalPages} pages.`,
      );
    }

    // repositoryのfindメソッドを呼び出す
    return this.dodoitsuRepository.find({
      order,
      take: limit,
      skip: (page - 1) * limit,
    });
  }
```

また、ドメインサービスではレスポンス用にDTO（Data Transfer Object）を扱うことは一般的にありません。
ドメイン層で扱うべきはドメインであり、ビジネスロジックとエンティティの管理に集中するべきです。

アプリケーション層でデータの成形を行います。
アプリケーション層はシステムと外部のクライアント（例えば、ユーザーインターフェースや外部API）とのインターフェースを担当しているからです。
アプリケーション層は、ドメイン層から取得したデータを、クライアントが必要とする形式に変換する役割を果たします。これには、DTOの作成とマッピングが含まれます。

#### アプリケーション層
アプリケーション層は、以下の三つに関心を置きます。
1. データの成形
2. セキュリティとバリデーション
3. クライアントとのインターフェース

##### DTO

リクエストボディのバリデーションと、レスポンスデータの成形を両方DTOで行っています。

<details><summary>create-dodoitsu.dto.ts</summary>

```ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator';

export class CreateDodoitsuDto {
  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  readonly content: string;

  @ApiProperty()
  @IsString()
  readonly description: string;
}
```
</details>

<details><summary>response-dodoitsu.dto.ts</summary>

```ts
import {
  IsString,
  IsNotEmpty,
  IsDate,
  IsBoolean,
  IsNumber,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';
import { ResponseUserDto } from '@application/user/dto/response-user.dto';

export class ResponseDodoitsuDto {
  constructor(dodoitsu: Dodoitsu, isLiked: boolean) {
    this.id = dodoitsu.id;
    this.content = dodoitsu.content;
    this.description = dodoitsu.description;
    this.createdAt = dodoitsu.createdAt;

    this.likeCount = dodoitsu.likes ? dodoitsu.likes.length : 0;
    this.isLiked = isLiked;
    if (dodoitsu.author) {
      this.author = new ResponseUserDto(dodoitsu.author);
    }
  }

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  readonly id: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  readonly content: string;

  @IsString()
  @ApiProperty()
  readonly description?: string;

  @IsDate()
  @IsNotEmpty()
  @ApiProperty()
  readonly createdAt: Date;

  @IsNumber()
  @IsNotEmpty()
  @ApiProperty()
  readonly likeCount: number;

  @IsBoolean()
  @IsNotEmpty()
  @ApiProperty()
  readonly isLiked: boolean;

  @ApiProperty()
  readonly author?: ResponseUserDto;
}
```
</details>

レスポンス用のDTOでは、constructorで受け取ったDodoitsu Entityに対し、「いいね数」をjoinされているDodoitsuLike Entityの数から集計したり、`ResponseUserDto`を使用し、不要なパラメータを削除したりしています。

<details><summary>response-user.dto.ts</summary>

```ts
import { IsString, IsNotEmpty, IsDate } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { User } from '@domain/user/user.entity';

export class ResponseUserDto {
  constructor(user: User) {
    this.id = user.id;
    this.name = user.name;
    this.photo = user.photo;
    this.twitterId = user.twitterId;
    this.createdAt = user.createdAt;
  }

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  readonly id: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  readonly name: string;

  @IsString()
  @ApiProperty()
  readonly photo: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  readonly twitterId: string;

  @IsDate()
  @ApiProperty()
  readonly createdAt: Date;
}
```
</details>

Userテーブルには実際にはリフレッシュトークン等が保存されていますが、DTOを使用し削っています。
typeormにはデフォルトで特定のカラムを返さないようにするデコレータもありますが、今後の汎用性・明示的にリフレッシュトークンを削る実装の方が可読性を向上させると考え、このような実装パターンを取っています。

##### コントローラー

<details><summary>dodoitsu.controller.ts</summary>

```ts
import {
  Req,
  Body,
  Controller,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  Query,
  Post,
  Delete,
  DefaultValuePipe,
  ParseIntPipe,
  UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiResponse } from '@common/ApiResponse';

import { DodoitsuApplicationService } from '@application/dodoitsu/dodoitsu.service';
import { CreateDodoitsuDto } from '@application/dodoitsu/dto/create-dodoitsu.dto';
import { ResponseDodoitsuDto } from '@application/dodoitsu/dto/response-dodoitsu.dto';

import { OptionalJwtAuthGuard } from '@application/auth/guards/optional-jwt-auth.guard';

@Controller('dodoitsu')
export class DodoitsuController {
  constructor(
    private readonly dodoitsuApplicationService: DodoitsuApplicationService,
  ) {}

  @UseGuards(OptionalJwtAuthGuard)
  @Get('latest')
  @HttpCode(HttpStatus.OK)
  async findLatest(
    @Req() req,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ): Promise<ApiResponse<ResponseDodoitsuDto[]>> {
    const [dodoitsuList, allCount] = await Promise.all([
      this.dodoitsuApplicationService.findLatestDodoitsu(page, limit, req.user),
      this.dodoitsuApplicationService.countAllDodoitsu(),
    ]);
    return ApiResponse.success(dodoitsuList, allCount);
  }

  @UseGuards(OptionalJwtAuthGuard)
  @Get('popular')
  @HttpCode(HttpStatus.OK)
  async findPopular(
    @Req() req,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ): Promise<ApiResponse<ResponseDodoitsuDto[]>> {
    const [dodoitsuList, allCount] = await Promise.all([
      this.dodoitsuApplicationService.findPopularDodoitsu(
        page,
        limit,
        req.user,
      ),
      this.dodoitsuApplicationService.countAllDodoitsu(),
    ]);
    return ApiResponse.success(dodoitsuList, allCount);
  }

  @UseGuards(OptionalJwtAuthGuard)
  @Get(':id')
  @HttpCode(HttpStatus.OK)
  async findOne(
    @Req() req,
    @Param('id') id: string,
  ): Promise<ApiResponse<ResponseDodoitsuDto | null>> {
    const dodoitsu = await this.dodoitsuApplicationService.findOneDodoitsu(
      id,
      req.user,
    );
    return ApiResponse.success(dodoitsu);
  }

  @UseGuards(OptionalJwtAuthGuard)
  @Post()
  @HttpCode(HttpStatus.OK)
  async create(
    @Req() req,
    @Body() createDodoitsuDto: CreateDodoitsuDto,
  ): Promise<ApiResponse<ResponseDodoitsuDto>> {
    const dodoitsu = await this.dodoitsuApplicationService.createDodoitsu(
      createDodoitsuDto,
      req.user,
    );
    return ApiResponse.success(dodoitsu);
  }

  @UseGuards(AuthGuard('jwt'))
  @Post(':id/like')
  async like(@Param('id') id: string, @Req() req): Promise<ApiResponse<any>> {
    await this.dodoitsuApplicationService.likeDodoitsu(id, req.user);
    return ApiResponse.success(null);
  }

  @UseGuards(AuthGuard('jwt'))
  @Delete(':id/unlike')
  @HttpCode(HttpStatus.OK)
  async unlike(@Param('id') id: string, @Req() req): Promise<ApiResponse<any>> {
    await this.dodoitsuApplicationService.unlikeDodoitsu(id, req.user.id);
    return ApiResponse.success(null);
  }

  @UseGuards(OptionalJwtAuthGuard)
  @Get('user/:userId')
  @HttpCode(HttpStatus.OK)
  async findByUserId(
    @Param('userId') userId: string,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ): Promise<ApiResponse<ResponseDodoitsuDto[]>> {
    const [dodoitsuList, allCount] = await Promise.all([
      this.dodoitsuApplicationService.findDodoitsuByUserId(userId, page, limit),
      this.dodoitsuApplicationService.countDodoitsuByUserId(userId),
    ]);
    return ApiResponse.success(dodoitsuList, allCount);
  }

  @UseGuards(OptionalJwtAuthGuard)
  @Get('user/:userId/liked')
  @HttpCode(HttpStatus.OK)
  async findLikedByUserId(
    @Param('userId') userId: string,
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ): Promise<ApiResponse<ResponseDodoitsuDto[]>> {
    const [dodoitsuList, allCount] = await Promise.all([
      this.dodoitsuApplicationService.findLikedDodoitsuByUserId(
        userId,
        page,
        limit,
      ),
      this.dodoitsuApplicationService.countLikedDodoitsuByUserId(userId),
    ]);
    return ApiResponse.success(dodoitsuList, allCount);
  }
}
```
</details>

Web APIなので、皆大好きControllerです。

controllerは、リクエストを受け取り、アプリケーションサービスに渡す、レスポンスするのみの役割となっています。

##### アプリケーションサービス

<details><summary>dodoitsu.service.ts</summary>

```ts
import { Injectable } from '@nestjs/common';
import { CreateDodoitsuDto } from '@application/dodoitsu/dto/create-dodoitsu.dto';
import { ResponseDodoitsuDto } from '@application/dodoitsu/dto/response-dodoitsu.dto';
import { DodoitsuService } from '@domain/dodoitsu/dodoitsu.service';
import { User } from '@domain/user/user.entity';

@Injectable()
export class DodoitsuApplicationService {
  constructor(private readonly dodoitsuDomainService: DodoitsuService) {}

  async countAllDodoitsu(): Promise<number> {
    return this.dodoitsuDomainService.countAll();
  }

  async findLatestDodoitsu(
    page: number,
    limit: number,
    user?: User,
  ): Promise<ResponseDodoitsuDto[]> {
    const dodoitsuList = await this.dodoitsuDomainService.findLatest(
      page,
      limit,
    );

    const responseDodoitsuList = await Promise.all(
      dodoitsuList.map(async (dodoitsu) => {
        const isLiked = user
          ? await this.dodoitsuDomainService.didUserLike(dodoitsu.id, user.id)
          : false;
        return new ResponseDodoitsuDto(dodoitsu, isLiked);
      }),
    );
    return responseDodoitsuList;
  }

  async findPopularDodoitsu(
    page: number,
    limit: number,
    user?: User,
  ): Promise<ResponseDodoitsuDto[]> {
    const dodoitsuList = await this.dodoitsuDomainService.findPopular(
      page,
      limit,
    );

    const responseDodoitsuList = await Promise.all(
      dodoitsuList.map(async (dodoitsu) => {
        const isLiked = user
          ? await this.dodoitsuDomainService.didUserLike(dodoitsu.id, user.id)
          : false;
        return new ResponseDodoitsuDto(dodoitsu, isLiked);
      }),
    );
    return responseDodoitsuList;
  }

  async findOneDodoitsu(
    id: string,
    user?: User,
  ): Promise<ResponseDodoitsuDto | null> {
    const dodoitsu = await this.dodoitsuDomainService.findOne(id);
    const isLiked = user
      ? await this.dodoitsuDomainService.didUserLike(id, user.id)
      : false;
    return new ResponseDodoitsuDto(dodoitsu, isLiked);
  }

  async createDodoitsu(
    dto: CreateDodoitsuDto,
    user?: User,
  ): Promise<ResponseDodoitsuDto> {
    const dodoitsu = await this.dodoitsuDomainService.create(dto, user);
    return new ResponseDodoitsuDto(dodoitsu, false);
  }

  async likeDodoitsu(id: string, user: User): Promise<void> {
    const isLiked = await this.dodoitsuDomainService.didUserLike(id, user.id);
    if (isLiked) {
      throw new Error('Already liked');
    }
    const dodoitsu = await this.dodoitsuDomainService.findOne(id);
    return this.dodoitsuDomainService.likeDodoitsu(dodoitsu, user);
  }

  async unlikeDodoitsu(id: string, user: User): Promise<void> {
    const dodoitsu = await this.dodoitsuDomainService.findOne(id);
    return this.dodoitsuDomainService.unlikeDodoitsu(dodoitsu, user);
  }

  async findDodoitsuByUserId(
    userId: string,
    page: number,
    limit: number,
  ): Promise<ResponseDodoitsuDto[]> {
    const dodoitsuList = await this.dodoitsuDomainService.findByUserId(
      userId,
      page,
      limit,
    );

    const responseDodoitsuList = await Promise.all(
      dodoitsuList.map(async (dodoitsu) => {
        const isLiked = await this.dodoitsuDomainService.didUserLike(
          dodoitsu.id,
          userId,
        );
        return new ResponseDodoitsuDto(dodoitsu, isLiked);
      }),
    );
    return responseDodoitsuList;
  }

  async countDodoitsuByUserId(userId: string): Promise<number> {
    return this.dodoitsuDomainService.countByUserId(userId);
  }

  async findLikedDodoitsuByUserId(
    userId: string,
    page: number,
    limit: number,
  ): Promise<ResponseDodoitsuDto[]> {
    const dodoitsuList = await this.dodoitsuDomainService.findLikedByUserId(
      userId,
      page,
      limit,
    );

    const responseDodoitsuList = await Promise.all(
      dodoitsuList.map((dodoitsu) => {
        return new ResponseDodoitsuDto(dodoitsu, true);
      }),
    );
    return responseDodoitsuList;
  }

  async countLikedDodoitsuByUserId(userId: string): Promise<number> {
    return this.dodoitsuDomainService.countLikedByUserId(userId);
  }
}
```
</details>

ドメインではなく、アプリケーションそのもののビジネスロジックをカプセル化する層です。
特定のビジネスプロセスやユースケースを実装するためのメソッドを提供します。
また、DTOを使用してのデータの成形も行っています。
トランザクションの管理等もアプリケーションサービスで行うものとなります。

##### .module.ts（機能のカプセル化）

<details><summary>dodoitsu.module.ts</summary>

```ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';

import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';
import { DodoitsuLike } from '@domain/dodoitsu/dodoitsu-like.entity';
import { SYMBOL as DODOITSU_SYMBOL } from '@domain/dodoitsu/dodoitsu.repository.interface';

import { User } from '@domain/user/user.entity';

import { DodoitsuController } from '@application/dodoitsu/dodoitsu.controller';
import { DodoitsuApplicationService } from '@application/dodoitsu/dodoitsu.service';
import { DodoitsuService } from '@domain/dodoitsu/dodoitsu.service';
import { UserModule } from '@application/user/user.module';

import { DodoitsuRepository } from '@infrastructure/orm/dodoitsu/dodoitsu.repository';
import { OptionalJwtStrategy } from '@infrastructure/auth/optional-jwt.strategy';

@Module({
  imports: [
    TypeOrmModule.forFeature([Dodoitsu, User, DodoitsuLike]),
    JwtModule,
    ConfigModule,
    UserModule,
  ],
  controllers: [DodoitsuController],
  providers: [
    DodoitsuApplicationService,
    DodoitsuService,
    {
      provide: DODOITSU_SYMBOL,
      useClass: DodoitsuRepository,
    },
    OptionalJwtStrategy,
  ],
  exports: [],
})
export class DodoitsuModule {}
```
</details>

Dodoitsu関連の機能をカプセル化するファイルです。
そろそろ、記述量が多くなってきて、Qiitaの編集画面が重いです。
コードは後から貼り付けるべきでした。

`.module.ts`はセクションに分かれています。
それぞれを掘り下げてみます。

###### imports

```ts
  imports: [
    TypeOrmModule.forFeature([Dodoitsu, User, DodoitsuLike]),
    JwtModule,
    ConfigModule,
    UserModule,
  ],
```

imports セクションでは、モジュールが依存する他のモジュールやライブラリをインポートしています。これにより、DodoitsuModule 内でこれらのモジュールやライブラリの機能を利用することができます。

- TypeOrmModule.forFeature([Dodoitsu, User, DodoitsuLike]): TypeORM を使用して、特定のエンティティ（ここでは Dodoitsu, User, DodoitsuLike）に対するリポジトリを提供します。これにより、これらのエンティティに対するデータベース操作が可能になります。
- JwtModule: JWT (JSON Web Tokens) 認証を利用するためのモジュールをインポートしています。これにより、JWT を使用した認証機能を実装することができます。
- ConfigModule: アプリケーションの設定を管理するためのモジュールをインポートしています。これにより、アプリケーションの設定情報を効率的に管理することができます。
- UserModule: User 関連の機能をカプセル化したモジュールをインポートしています。これにより、User 関連の機能を DodoitsuModule 内で利用することができます。

###### controllers

```ts
  controllers: [DodoitsuController],
```

controllers セクションでは、HTTP リクエストをハンドリングするコントローラを定義しています。ここでは DodoitsuController が指定されており、Dodoitsu 関連のリクエストを処理する責務を持っています。

###### providers

```ts
  providers: [
    DodoitsuApplicationService,
    DodoitsuService,
    {
      provide: DODOITSU_SYMBOL,
      useClass: DodoitsuRepository,
    },
    OptionalJwtStrategy,
  ],
```

providers セクションでは、モジュール内で使用するサービスやリポジトリなどのプロバイダを定義しています。

- DodoitsuApplicationService: アプリケーションサービスを提供しており、アプリケーションのビジネスロジックをカプセル化しています。
- DodoitsuService: ドメインサービスを提供しており、ドメインのビジネスロジックをカプセル化しています。
- provide: DODOITSU_SYMBOL, useClass: DodoitsuRepository: カスタムプロバイダを使用して、DodoitsuRepository を DODOITSU_SYMBOL というトークンで注入できるようにしています。
- OptionalJwtStrategy: JWT 認証のストラテジーを提供しています。

###### exports

このコードでは何もexportしていませんが、exportすれば、userModuleのように、importsすればほかのモジュールの機能が使えるようになります。

#### インフラストラクチャ層

インフラストラクチャ層では、データベースや、外部APIへのアクセスを主に扱います。
ロギング、セキュリティ、通知、エラーハンドリングなど、アプリケーション全体にわたる懸念をサポートします。

##### ストラテジ

<details><summary>jwt.strategy.ts</summary>

```ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { UserService } from '@domain/user/user.service';
import { User } from '@domain/user/user.entity';

export interface JwtPayload {
  userId: string;
}

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(
    private readonly userService: UserService,
    private readonly configService: ConfigService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: true,
      secretOrKey: configService.get<string>('auth.jwt.secret'),
    });
  }

  async validate(payload: JwtPayload): Promise<User> {
    const user = await this.userService.findOne({ id: payload.userId });
    if (!user) {
      throw new UnauthorizedException('User does not exist');
    }

    return user;
  }
}
```
</details>

こんな感じのやつです。
`passport`を使用して、ユーザーの認証を行うためのやつです

##### ormを使用したDBへのアクセス

repositoryの実体です。
超ベーシックです。
<details><summary>dodoitsu.repository.ts</summary>

```ts
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';

import {
  IDodoitsuRepository,
  FindOptions,
} from '@domain/dodoitsu/dodoitsu.repository.interface';
import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';
import { DodoitsuLike } from '@domain/dodoitsu/dodoitsu-like.entity';

import { User } from '@domain/user/user.entity';
import { CreateDodoitsuDto } from '@application/dodoitsu/dto/create-dodoitsu.dto';

@Injectable()
export class DodoitsuRepository implements IDodoitsuRepository {
  constructor(
    @InjectEntityManager()
    private readonly entityManager: EntityManager,
  ) {}

  async find(options: FindOptions): Promise<Dodoitsu[]> {
    const dodoitsuList = await this.entityManager.find(Dodoitsu, {
      ...options,
      relations: ['author', 'likes'],
    });
    return dodoitsuList;
  }

  async findWithLikesOrder(options: FindOptions): Promise<Dodoitsu[]> {
    const dodoitsuList = await this.entityManager
      .getRepository(Dodoitsu)
      .createQueryBuilder('dodoitsu')
      .leftJoinAndSelect('dodoitsu.likes', 'dodoitsuLike')
      .leftJoinAndSelect('dodoitsu.author', 'user')
      .groupBy('dodoitsu.id, user.id, dodoitsuLike.id')
      .addSelect('COUNT(dodoitsuLike.id)', 'likes_count')
      .orderBy('likes_count', 'DESC')
      .skip(options.skip)
      .take(options.take)
      .getMany();

    return dodoitsuList;
  }

  async findOne(id: string): Promise<Dodoitsu> {
    const dodoitsu = await this.entityManager.findOne(Dodoitsu, {
      where: { id },
      relations: ['author', 'likes'],
    });

    return dodoitsu;
  }

  async findByUser(options: FindOptions, userId: string): Promise<Dodoitsu[]> {
    const dodoitsuList = await this.entityManager.find(Dodoitsu, {
      where: { author: { id: userId } },
      ...options,
      relations: ['author', 'likes'],
    });
    return dodoitsuList;
  }

  async countByUser(userId: string): Promise<number> {
    return await this.entityManager.count(Dodoitsu, {
      where: { author: { id: userId } },
    });
  }

  async findLikedByUser(
    options: FindOptions,
    userId: string,
  ): Promise<Dodoitsu[]> {
    const likedDodoitsuList = await this.entityManager
      .getRepository(DodoitsuLike)
      .createQueryBuilder('dodoitsuLike')
      .leftJoinAndSelect('dodoitsuLike.dodoitsu', 'dodoitsu')
      .leftJoinAndSelect('dodoitsu.author', 'user')
      .leftJoinAndSelect('dodoitsu.likes', 'dodoitsu_like')
      .where('dodoitsuLike.user.id = :userId', { userId })
      .skip(options.skip)
      .take(options.take)
      .getMany();

    return likedDodoitsuList.map((like) => like.dodoitsu);
  }

  async countLikedByUser(userId: string): Promise<number> {
    return await this.entityManager.count(DodoitsuLike, {
      where: { user: { id: userId } },
    });
  }

  async count(): Promise<number> {
    return await this.entityManager.count(Dodoitsu);
  }

  async create(
    createDodoitsuDto: CreateDodoitsuDto,
    author?: User,
  ): Promise<Dodoitsu> {
    const dodoitsu = new Dodoitsu();
    dodoitsu.content = createDodoitsuDto.content;
    dodoitsu.description = createDodoitsuDto.description;
    if (author) {
      dodoitsu.author = author;
    }
    return await dodoitsu;
  }

  async save(dodoitsu: Dodoitsu): Promise<Dodoitsu> {
    const newDodoitsu = await this.entityManager.save(dodoitsu);
    return newDodoitsu;
  }

  like(dodoitsu: Dodoitsu, user: User): void {
    const newLike = new DodoitsuLike();
    newLike.dodoitsu = dodoitsu;
    newLike.user = user;
    this.entityManager.save(DodoitsuLike, newLike);
  }

  unlike(dodoitsu: Dodoitsu, user: User): void {
    this.entityManager.delete(DodoitsuLike, { dodoitsu, user });
  }

  async didUserLike(dodoitsuId: string, userId: string): Promise<boolean> {
    const like = await this.entityManager.findOne(DodoitsuLike, {
      where: {
        dodoitsu: { id: dodoitsuId },
        user: { id: userId },
      },
    });

    return !!like;
  }
}

```
</details>

##### DB接続設定、マイグレーションとか

<details><summary>typeorm-util.ts</summary>

```ts
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

import { Dodoitsu } from '@domain/dodoitsu/dodoitsu.entity';
import { DodoitsuLike } from '@/domain/dodoitsu/dodoitsu-like.entity';
import { User } from '@domain/user/user.entity';

export const createTypeOrmOptions = async (
  configService: ConfigService,
): Promise<TypeOrmModuleOptions> => {
  const ssl = configService.get('isLocal')
    ? false
    : {
        rejectUnauthorized: false,
      };

  return {
    name: 'default',
    type: 'postgres',
    host: configService.get('database.host'),
    port: configService.get('database.port'),
    username: configService.get('database.username'),
    password: configService.get('database.password'),
    database: configService.get('database.database'),
    logging: configService.get('database.logging'),
    ssl,
    synchronize: configService.get('database.synchronize'),
    entities: [Dodoitsu, DodoitsuLike, User],
    migrations: ['dist/migration/*.js'],
  };
};
```
</details>

記事書いてて思いました。ファイル名が変な気がします。
`connection-options.ts`みたいな感じに脳内変換いただけると嬉しいです。

## おわりに

以上、フロント畑ながらも、API開発のアーキテクチャを頑張ってみた話でした。

良ければ[twitterフォロー](https://twitter.com/fal_engineer)、Qiitaのフォロー等よろしくお願いします～