【フォースタ テックブログ】RepositoryFactoryパターンをVueのAPIリクエストに導入する - for Startups Tech blog Nuxt.js × typescriptで実装する api repositoryFactoryパターン | スマートショッピング 【Vue.js】Web API通信のデザインパターン (個人的ベストプラクティス) - Qiita

RepositoryFactoryパターンとは

APIを呼び出す設計のデザインパターンとして、JorgeというVueエヴァンジェリストによって紹介された。 Vue API calls in a smart way

  • Repositoryによって、axiosインスタンスをコンポーネントに直接書くのを避けることができる
    • データの操作をビジネスロジックと分離する
    • エンドポイントの変更に強くなる
    • 再利用性が高まる
  • Factoryによって、各ケースで必要なリポジトリをインスタンス化する
    • コンポーネントはRepositoryの実体化の方法を知らなくていい
    • テストのためのmockがしやすくなる

準備

import { Context } from '@nuxt/types'
import { Inject, Plugin } from '@nuxt/types/app'
import {
  UserRepository,
} from '~/repositories/api'
 
/**
 * APIリクエストのインターフェース
 * グローバルに$repositoriesで呼び出すことができる
 * unit testのときはrepositoriesを差し替えることでmock可能
 */
export interface RepositoryApis {
  user: UserRepository
}
 
const plugin: Plugin = ({ $axios, $config }: Context, inject: Inject) => {
  const user = new UserRepository($axios)
 
  const repositories: RepositoryApis = {
    user,
  }
  inject('repositories', repositories)
}
 
export default plugin
import { NuxtConfig } from '@nuxt/types'
 
const config: NuxtConfig = {
    
  // ...
  plugins: [
    '~/plugins/repository-api',
  ],
 
}
 
export default config
 
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Consola } from 'consola'
import { Store } from 'vuex'
import { RepositoryApis } from '~/plugins/repository-api'
 
// axios, consola は関係ないが使うので入れておくとしたらこんな書き方
declare module 'vue/types/vue' {
  export interface Context {
    $axios: NuxtAxiosInstance
    $logger: Consola
    $repositories: RepositoryApis
  }
  interface Vue {
    $logger: Consola
    $repositories: RepositoryApis
  }
}
 
declare module '@nuxt/types' {
  export interface Context {
    $axios: NuxtAxiosInstance
    $logger: Consola
    $repositories: RepositoryApis
  }
  interface NuxtAppOptions {
    $logger: Consola
    $repositories: RepositoryApis
  }
}
 
declare module 'vuex/types' {
  interface Store<S> {
    $repositories: RepositoryApis
  }
}
 
import type { AxiosInstance } from 'axios'
 
export class UserRepository {
  private readonly axios: AxiosInstance
 
  constructor($axios: AxiosInstance) {
    this.axios = $axios
  }
 
  async search(id: string): Promise<User> {
    // this.$axios.get みたいな処理
  }
}

使い方

Vueコンポーネント内で $repositories で使用できる

async fetchUser(id: string) {
    const data = await this.$repositories.user.search(id)
    return data
}

テスト

import { MockProxy, mock } from 'jest-mock-extended'
import { Context } from '@nuxt/types'
import { RepositoryApis } from '~/plugins/repository-api'
import { UserRepository } from '~/repositories/api'
 
describe('user', () => {
  let mockContext: MockProxy<Context>
  let repositories: MockProxy<RepositoryApis>
 
  beforeEach(() => {
    const mockUser = mock<UserRepository>()
    repositories = mock<RepositoryApis>()
    repositories.user = mockUser
 
    mockContext.$repositories = repositories
  })
    
  it('do something', () => {
    doSomething(mockContext)
 
    expect(repositories.user.search).toHaveBeenCalledTimes(1)
    expect(repositories.user.search).toHaveBeenCalledWith({
      id: 'foo',
    })
  })
 
})