🍱

Nuxt.js x Typescript で API 通信のデザインパターンを使ってみた

4 min read

はじめに

今回は API リクエストの処理を切り分けて管理するパターンを知ったので、 Nuxt.js x TypeScript のプロジェクトで実際に試してみました。もっと良い書き方などあれば、コメント欄で教えてください。

このパターンを適用する目的は以下です。

  • エンドポイントのパス変更などが発生した際にロジックへの影響を切り離す
  • データアクセスのコードを再利用可能にする
  • テスタブルにする

なお、バックエンドは RESTful API を想定しています。

環境

  • Nuxt.js 2.x
  • Vue 2
  • vue-composition-api
  • TypeScript

Repository

まずは Repository です。
RESTful API の CRUD に合わせて、index, show, create, update, delete メソッドを用意します。

api/repository.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios';
import { AxiosResponse } from 'axios'

export interface CRUDActions {
  index<T>(query?: string): Promise<AxiosResponse<T>>
  show<T>(id: number): Promise<AxiosResponse<T>>
  create<T>(payload: any): Promise<AxiosResponse<T>>
  update<T>(payload: any, id?: number): Promise<AxiosResponse<T>>
  delete(id?: number): Promise<AxiosResponse<any>>
}

export default (client: NuxtAxiosInstance) => (resource: string) => ({
  index<T>(query?: string) {
    return client.get<T>(`api/${resource}?${query}`)
  },
  show<T>(id: number) {
    return client.get<T>(`api/${resource}/${id}`)
  },
  create<T>(payload: any) {
    return client.post<T>(`api/${resource}`, payload)
  },
  update<T>(payload: any, id?: number) {
    return client.patch<T>(resourcePath(resource, id), payload)
  },
  delete(id?: number) {
    return client.delete(resourcePath(resource, id))
  }
})

const resourcePath = (resource: string, id?: number) => {
  let path
  if (id) {
    path = `api/${resource}/${id}`
  } else {
    path = `api/${resource}`
  }
  return path
}

RESTful API では has_one 関係にある場合、id を含まずに単数形のパスで取得することがあります。例: api/users/bank_account

そのケースを考慮すると resourcePath メソッドのように id の有無に対して分岐が必要になります。(もっと良い方法はあるだろうか)

RepositoryFactory

次に repository-factory です。

plugins/repository-factory.ts
import createRepositories, { CRUDActions } from '@/api/repository'

export interface Repositories {
  clients: CRUDActions
  // リソースが増えるごとに追加する
}

export default (context: any, inject: any) => {
  const repositoryWithAxios = createRepositories(context.$axios)
  const repositories = {
    clients: repositoryWithAxios('clients') // リソース名とマッピングする
  }
  inject('repositories', repositories)
}

最後に context.root した時に型情報を参照できるように以下のように記述します。

nuxt.config.ts
import { Configuration } from '@nuxt/types'
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Repositories } from '@/plugins/repository-factory'

const nuxtConfig: Configuration = {
  // 省略
}

declare module 'vue/types/vue' {
  interface Vue {
    $axios: NuxtAxiosInstance
    $repositories: Repositories // 追加
  }
}

これで準備は完了です。

使用例

以下は実際の使用例です。

clients.vue
<template>...</template>

<script lang="ts">
import { defineComponent, provide } from '@vue/composition-api'
import clientStore from '@/store/client'
import ClientKey from '@/store/client-key'

export default defineComponent({
  setup(_, context) {
    const store = clientStore()
    store.actions.getData(context.root.$repositories) // repositories を渡す

    provide(ClientKey, store)
  }
})
</script>
store/client.ts
import { Repositories } from '@/plugins/repository-factory'
import { ClientResponse } from '@/types/model/client'

..

const actions = {
  async getData(repositories: Repositories) {
    try {
      const res = await repositories.clients.index<ClientResponse[]>() // repository を利用
      ...
    } catch (error) {
      // error handling
    }
  },
},

無事、データへアクセスする処理は隠蔽することができました。型の補完もバッチリ効いています。

感想

扱うリソースが増えた場合でも repository-factory.ts の必要な箇所にリソース名を追加すれば、データアクセスの処理は再利用できるので便利です。テストも書きやすくなりました。

参考リンク

Discussion

ログインするとコメントできます