Vue 3 + TypeScript ではじめるリポジトリパターン

# はじめに

Vue 3系 + TypeScript での API によるデータ操作をリポジトリパターンを用いてまとめたときの実装内容を、実際の手順とコードを踏まえつつまとめます。

## リポジトリパターンとは

[リポジトリパターン](https://docs.microsoft.com/ja-jp/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design#the-repository-pattern) とは、データ操作の処理を一元管理することで、ビジネスロジックからデータ操作ロジックを切り離すデザインパターンです。

これにより、データ操作ロジックの保守性・再利用性を向上させることができます。

## つまり

* axios の多重管理が発生し、エンドポイントや axios のプロパティに変更があった際など保守するのが大変
* コンポーネント内で同じようなデータ操作ロジックを何度も書くのが大変
* コンポーネント内でのデータ操作ロジックの直書きによって単体テストが大変

このような大変を解消してくれるのが、リポジトリパターンです。

# サンプルコード

では、実際に手を動かしてみていきましょう。

## 1. プロジェクトの作成

vue-cli を利用してプロジェクトを作成しましょう。
参考までに筆者の設定。

```sh
$ vue create frontend

Vue CLI v4.5.15
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
```

ベースとなるプロジェクトが作成されました。
最初は下記のようなディレクトリ構成になっていると思います。

```sh
frontend
├── node_modules
├── public
├── src
│   ├── assets
│   ├── components
│   ├── router
│   ├── store
│   └── views
└── tests
    └── unit
```

## 2. リポジトリの実装

ここからはリポジトリパターンを用いた API 実行に必要なリソースを作成していきましょう。
まずは、関連ファイルを一元管理するために `src` 配下に `apis` フォルダを作成し、そこに３種類のファイルを作成していきます。

```sh
frontend
├── node_modules
├── public
├── src
│   ├── apis # 追加
│   │   ├── repository.ts # 追加
│   │   ├── repositoryFactory.ts # 追加
│   │   └── userRepository.ts # 追加
│   ├── assets
│   ├── components
│   ├── router
│   ├── store
│   └── views
└── tests
    └── unit
```

### 2-1. repository.ts

API を実行する axios のインスタンスを管理するファイルです。
通信を行うエンドポイントを指定した axios のインスタンスを作成し、これを使いまわすことでコンポーネント内で axios インスタンスを多重管理することを避けます。

```repository.ts
import axios from 'axios';

const baseURL = 'https://sample-baseurl/api/v1';

export default axios.create({
  baseURL
});
```

また、[.env ファイル](https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code) を環境ごとに作成してエンドポイントを管理すれば、本番環境・開発環境などで axios が指すエンドポイントの切り替えが簡単にできるようになります。（`.env` ファイルの詳細な説明については長くなるので当記事では省略します。）

```..env.local
VUE_APP_API_BASE_URL = 'https://sample-baseurl/api/v1'
```

```repository.ts
// const baseURL = 'https://sample-baseurl/api/v1';
const baseURL = process.env.VUE_APP_API_BASE_URL;
```

### 2-2. userRepository.ts

実際のデータ操作ロジックを記載するファイルです。
ここではサンプルとして、ユーザー情報のデータ操作をするためのリポジトリを作成します。

```userRepository.ts
import { AxiosPromise } from 'axios';
import repository from '@/apis/repository';

const resource = '/users';

export interface User {
  userId: number;
  userName: string;
}

export default class UserRepository {
  public getUsers(): AxiosPromise<User[]> {
    return repository.get<User[]>(`${resource}`);
  }

  public getUser(id: number): AxiosPromise<User> {
    return repository.get<User>(`${resource}/${id}`);
  }

  public createUser(payload: User): AxiosPromise<User> {
    return repository.post<User>(`${resource}`, payload);
  }

  public updateUser(id: number, payload: User): AxiosPromise<User> {
    return repository.put<User>(`${resource}/${id}`, payload);
  }

  public deleteUser(id: number): AxiosPromise<void> {
    return repository.delete<void>(`${resource}/${id}`);
  }
}
```
直前に作成した `repository.ts` をインポートし、各種 HTTP リクエストを実行する処理を記述しました。

### 2-3. repositoryFactory.ts

データ操作をするリポジトリを一元管理する役割を持つファイルです。
外部からインポートした際にリポジトリ群を返却するようにしておきます。

これによりインポート先で各リポジトリの処理を実行することができます。

```repositoryFactory.ts
import UserRepository from '@/apis/userRepository';

export interface Repositories {
  users: UserRepository;
  // リソースが増えた場合は適宜追加していけばよい
}

function getRepositories(): Repositories {
  const users = new UserRepository();
  const repositories: Repositories = {
    users,
  };
  return repositories;
}

export default getRepositories();
```

## 3. プロジェクト内で呼び出せるようにする

各 Vue コンポーネントでインポートして利用しても勿論問題ありませんが、今回は [globalProperties](https://v3.ja.vuejs.org/guide/typescript-support.html#globalproperties-%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E5%9E%8B%E3%81%AE%E6%8B%A1%E5%BC%B5) を利用してみます。

グローバルプロパティを作成するために、`main.ts` を下記のように変更します。
[各 Vue インスタンスで宣言する変数名と衝突しないよう](https://jp.vuejs.org/v2/cookbook/adding-instance-properties.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E9%87%8D%E8%A6%81%E6%80%A7) に、グローバルプロパティの接頭辞に `$` をつけましょう。


```main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import repository from '@/apis/repositoryFactory'; // 追加

const app = createApp(App).use(store).use(router);
app.config.globalProperties.$repository = repository; // 追加

app.mount('#app');
```

このままでは `$repository` の型推論ができずエラーになるので、新たに `repository.d.ts` を作成し、型宣言をします。

```repository.d.ts
import { Repositories } from '@/apis/repositoryFactory';

declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $repository: Repositories;
  }
}
```

さて、これで準備が整いました。
あとはコンポーネント内で `$repository` を呼び出すのみで API が実行できるようになります。

```Home.vue
<!-- 省略 -->

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Home',
  mounted() {
    this.$repository.users.getUsers(); // こんな感じ
  }
});
</script>
```

# おわりに

実際に利用してみて、ビジネスロジックとの分離によりそれぞれのファイルの責務が明確になり、見通しのよいコードを書くことができるようになったと感じます。

今現在コンポーネントのあちこちで axios をインスタンス化していたりして、データ操作ロジックの管理に困っている方の参考になれば幸いです。



## 参考文献

https://qiita.com/07JP27/items/0923cbe3b6435c19d761