Nuxt.js × typescriptで実装する api repositoryFactoryパターン

TypeScript JavaScript Vue.js Nuxt.js

hiroki_hayashi

2020-07-01

はじめに

エンジニアリング事業本部の@hiroki_hayashiです。

2019年7月から始まったsmartmat cloudのフルリプレイス完了から3ヶ月程度が経過しました。

大きな新規機能も無事リリースされ、細かい不具合改善や新規機能開発もスムーズに行えるようになりました。

リリース後に、私が担当する領域でフロントエンドにおいてAPI request周りに大きな変更を施したので

その際の当初の実装の失敗と改善点をご紹介できればと思います。

実装した動機

改修をした動機は以下の2点です。

  • API Version管理
  • Unit Test

API Version管理

従来の実装だと、APIのバージョン管理については行うことはなくenvにbaseURLにバージョンが含まれているといった状態でした。

env dev
https://dev-hoge.work/api/bff/v1

上記の実装の場合、一部のAPIだけがV2に差し変わるとなった場合どうしようもありません。

このどうしようもない状態がリリースわずか2ヶ月程度で発生してしまった、というのが今回の主な動機になります....

Unit Test

従来の実装では、各種component及びstoreからinjectされた$axiosを呼び出すことでapi requestを行なっていました。

そのためcomponentとロジックが密結合されてしまい、unitテストが非常に書きづらいものとなっていました。

repositoryパターンの採用

上記の問題を解決するため、repository factory パターンを採用しました。

repositoryパターンはリソースへのアクセスを分離します。

また、それぞれのロジックで引数でAPI Versionを指定する方針を取っています。

repository での具体的な実装

// @/api/hogeRepository.ts

import { NuxtAxiosInstance } from '@nuxtjs/axios'

type queryData = {
  q: string | null
}

export class HogeRepository {
  private readonly axios: NuxtAxiosInstance
  constructor($axios: NuxtAxiosInstance) {
    this.axios = $axios
  }

  createResource(apiVersion: Number) {
    return `v${apiVersion}/Hoge`
  }

  get(data: queryData, version = 1) {
    const uri = `${this.createResource(version)}/search`
    return this.axios.$get(uri, {
      params: { ...data }
    })
  }
}

@/api下にrepositoryを実装します。

クラス定義のみを行い、以下のfactoryでインスタンス化します。

上述の通り、それぞれのメソッドでAPI Versionとrequest及びqueryを受け取ります。

このような実装にはもう一つの利点があります。

仮にAPIが未実装の場合、getメソッドでstaticなjsonを返すことで、アプリケーション側の実装をそのままにAPIつなぎこみが可能です。

// plugin/repository.ts

import { HogeRepository } from '@/api/hogeRepository'

export interface Repositories {
  hoge: HogeRepository
}

export default function({ $axios }, inject) {
  const hoge = new HogeRepository($axios)
  const repositories: Repositories = {
    hoge
  }
  inject('repositories', repositories)
}

pluginにて実際にインスタンス化した各種repositoryをinjectします。

これでstoreやcomponent側で利用できます。

しかしこれでは、エディタによる型補完が効かないので以下のような形でvueのinterfaceを拡張してあげます。

import { Repositories } from '@/plugins/repository'

declare module 'vue/types/vue' {
  interface Vue {
    readonly $repositories: Repositories
  }
}

declare module 'vuex' {
  interface Store<S> {
    readonly $repositories: Repositories
  }
}

最後に実際にアプリケーション側での実装を見てみます。

<script lang="ts">
// 一部省略
methods: {
  async fetchData(ctx) {
    const queryData = {
      q: this.q,
    }
    await this.$repositories.hoge
      .get(queryData, 2)
      .then(({ hoges }) => {
        this.hoges = hoges
      })
      .catch((err) => {
        console.error(err)
      })
  }
}

repositoryパターンでのUnit Test

単体テストを書く場合、今回定義した$repositories.hogeをmockにして注入するだけでテストが可能です。

今回のようなgetをテストをする際は、staticなjsonファイルをimportすることでテストを容易に行うことができます。

単体テストの方針などはまたの機会にでも。。。

まとめ

今回紹介したrepository パターンでの実装は、個人的には今の所特段大きな不便は感じておらず、

次に何か新しいプロジェクトを実装する場合は、間違いなく採用しようと思っています。

スマートショッピングでは、一緒に働く仲間を探してます!
スマートショッピングの募集ポジション
JOB POSITIONS