TypeScript×クリーンアーキテクチャで簡単なタスク管理アプリを作成した

n 番煎じかもしれませんがクリーンアーキテクチャについての学習のアウトプットも兼ねて TypeScript で簡単なタスク管理アプリケーションを作成しました。

https://github.com/guitarinchen/clean-architecture-todo-app

## 使用技術

- [TypeScript](https://www.typescriptlang.org/) (言語)
- [Hono](https://hono-ja.pages.dev/) (Web フレームワーク)
- [Prisma](https://www.prisma.io/) (ORM)
- [Biome](https://biomejs.dev/) (linter, formatter)


## ディレクトリ構成

上層レイヤから infrastructure, presentation, application, domain の順

```tree
./src
├── application  // アプリケーション層。ユースケースを定義し、DTO を使用してデータの転送を管理。
│   ├── dtos  // データ転送オブジェクトを格納するディレクトリ
│   │   └── TaskDTO.ts
│   └── useCases  // ユースケースを定義するディレクトリ
│       ├── CreateTask.ts
│       ├── DeleteTask.ts
│       ├── FindAllTasks.ts
│       ├── FindTaskById.ts
│       └── UpdateTask.ts
├── domain  // ドメイン層。ビジネスロジックやルールを定義。
│   ├── entities  // ドメインエンティティを格納するディレクトリ
│   │   └── Task.ts
│   ├── errors  // エラー管理を行うためのクラスを格納するディレクトリ
│   │   ├── DatabaseError.ts
│   │   ├── UnknownError.ts
│   │   └── ValidationError.ts
│   ├── repositories  // リポジトリインターフェースを定義するディレクトリ
│   │   └── TaskRepository.ts
│   └── services  // ドメインサービスを定義するディレクトリ
│       └── TaskService.ts
├── index.ts  // アプリケーションのエントリーポイント
├── infrastructure  // インフラ層。外部システムとのやり取りを管理。
│   └── repositories  // リポジトリの実装を格納するディレクトリ
│       └── Prisma  // Prisma を使用したリポジトリの実装
│           └── PrismaTaskRepository.ts
└── presentation  // プレゼンテーション層。ユーザーとのインターフェースを管理。
    ├── controllers  // コントローラーを格納するディレクトリ
    │   ├── BaseController.ts
    │   └── TaskController.ts
    └── routes  // ルーティングを定義するディレクトリ
        └── taskRoutes.ts


```

## リポジトリ実装のエラーハンドリング

リポジトリ実装で例えば [Prisma](https://www.prisma.io/) や [node-postgres](https://node-postgres.com/) 等のパッケージに依存したエラーをそのまま投げてしまうと、内側の層が一番外側の infrastructure 層に依存するという依存性の逆転が発生してしまいます。そのためリポジトリ実装で発生したエラーは [domain 層で定義したカスタムエラー](https://github.com/guitarinchen/clean-architecture-todo-app/tree/main/src/domain/errors)に変換することで依存性の逆転が発生しないようにしました。

```ts
export class PrismaTaskRepository implements TaskRepository {
  constructor(private readonly client: PrismaClient) {}

  private handlePrismaError(e: unknown): Error {
    // エラー内容をログに残す
    console.error(e);
    // Prisma ORM に依存したエラーをカスタムエラーに変換
    if (e instanceof PrismaClientValidationError)
      return new ValidationError(e.message);
    if (e instanceof PrismaClientKnownRequestError)
      return new DatabaseError(e.message);
    if (
      e instanceof PrismaClientUnknownRequestError ||
      e instanceof PrismaClientRustPanicError
    )
      return new UnknownError(
        "An unknown error occurred while trying to fetch tasks",
      );

    return new UnknownError(
      "An unknown error occurred while trying to fetch tasks",
    );
  }

  async findAll() {
    try {
      const tasks = await this.client.task.findMany();
      return tasks.map((task) => new Task(task));
    } catch (e) {
      // エラーハンドリング
      throw this.handlePrismaError(e);
    }
  }

  // 以下省略
}
```

## おまけ

### Hono

本筋とは逸れますが今回採用した Web フレームワーク [Hono](https://hono-ja.pages.dev/) は Web 標準に準拠しています。そのため Web 標準の [Request](https://developer.mozilla.org/ja/docs/Web/API/Request), [Response](https://developer.mozilla.org/ja/docs/Web/API/Response), etc... を利用したコントローラを容易に作成することができました。例えば有名な Web フレームワークである [Express](http://expressjs.com/) では独自の Request や Response を採用しているため、それらを利用したコントローラを作成してしまうとフレームワークを変更した際にコントローラの実装も大きく変更する必要が出てくるかもしれません。

## 参考

- [実装クリーンアーキテクチャ](https://qiita.com/nrslib/items/a5f902c4defc83bd46b8)
- [クリーンアーキテクチャ完全に理解した](https://gist.github.com/mpppk/609d592f25cab9312654b39f1b357c60)