💻

(Nuxt + Composition API) RepositoryFactoryパターンを用いたAPIリクエストの実装サンプル

2021/10/05に公開

1. 概要

RepositoryFactoryパターン(VueにおけるAPI呼び出しのデザインパターン)の実装例を、Nuxt + Composition API + TypeScript の構成で紹介します。

実装例のリポジトリはこちらになります。

1.1. 本記事の背景

【フォースタ テックブログ】RepositoryFactoryパターンをVueのAPIリクエストに導入する | TechLab. blog にRepositoryFactoryパターンの非常にわかりやすい説明と、実装の一部が紹介されています。

上記記事を参考に Nuxt + Composition API + TypeScript の構成で実装する際、型の指定などでつまづくところがあったため、問題解決の備忘録として本記事を執筆しました。

ツール、ライブラリのバージョンは下記の通りです。

項目 ver
Node.js 15.9.0
Yarn 1.22.11
Nuxt 2.15.8

2. 詳細

2.1. 事前に必要なもの

Node.js、Yarn のインストール(Yarn は npm でも可。npmの場合は適宜読み替えてください。)

2.2. Nuxt プロジェクトのセットアップ

2.2.1. プロジェクトを作成

$ yarn create nuxt-app <project-name>

<project-name>は好きな名前を指定してください。

いくつか質問されるので答えていきます。今回は下記のように設定しました。

yarn create nuxt-app repository-factory-pattern-sample
yarn create v1.22.11
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-nuxt-app@3.7.1" with binaries:
      - create-nuxt-app
[##################################################################################################################################] 342/342
create-nuxt-app v3.7.1
✨  Generating Nuxt.js project in repository-factory-pattern-sample
? Project name: repository-factory-pattern-sample
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git

プロジェクト作成完了後、ディレクトリを移動(cd <project-name>)して下記コマンドで Nuxt のバージョンを確認することができます。

❯ npx nuxt -v
@nuxt/cli v2.15.8

また、下記コマンドでアプリケーションを立ち上げ、localhost にアクセスして動作するか確認しておきます。

yarn dev

図のような画面が表示されればOKです。
nuxt-startup-screen

2.2.2. ライブラリのインストール

axios と Composition API のライブラリを追加します。

$ yarn add @nuxtjs/axios
$ yarn add @nuxtjs/composition-api

ライブラリ追加後、nuxt.config.js に設定を追記します。

// nuxt.config.js
export default {
  ...,

  buildModules: [
    '@nuxt/typescript-build',
    '@nuxtjs/composition-api/module' // <- これを追記
  ],

  ...,

  modules: [
    '@nuxtjs/axios' // <- これを追記
  ]
}

また、モックAPIサーバーとして json-server を追加します。

$ yarn add -D json-server

プロジェクトルートにdb.jsonファイルを作成します

// db.json
{
  "user": [
    { "id": 1, "name": "test-user1", "mail": "test-user1@test.com" },
    { "id": 2, "name": "test-user2", "mail": "test-user2@test.com" },
    { "id": 3, "name": "test-user3", "mail": "test-user3@test.com" }
  ],
  "books": [
    { "id": 1, "title": "test1", "author": "test-user1" },
    { "id": 2, "title": "test2", "author": "test-user2" },
    { "id": 3, "title": "test3", "author": "test-user3" }
  ]
}

package.jsonにモックサーバーと同時に起動するスクリプトを追記します。

// package.json
{
  ...,
  "scripts": {
    ...,
    "dev:server": "json-server --watch ./db.json nuxt --port 3001 & nuxt", // <- 追記
    ...
  },
  ...
}

yarn dev:server を実行して、localhost:3001/user などにアクセスし、正常にレスポンスが返ってくればOKです。

各リソースごとのRepositoryの作成

今回はモックデータにuserbooksというリソースを準備したので、それぞれRepositoryを作成します。

// /composables/repositories/userRepository.ts
import { NuxtAxiosInstance } from "@nuxtjs/axios";

const resource = '/user'

export const UserRepository = ($axios: NuxtAxiosInstance) => ({
  get() {
    return $axios.$get(`${resource}`)
  }
})
// /composables/repositories/booksRepository.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'

const resource = '/books'

export const BooksRepository = ($axios: NuxtAxiosInstance) => ({
  get() {
    return $axios.get(`${resource}`)
  }
})

今回はgetメソッドしか定義していませんが、必要に応じてリソースごとにapiリクエストのメソッドを追加していきます。

Factoryの作成

Repository作成後、Factoryを作成していきます。

// /composables/factories/apiRepositoryFactory.ts
import { UserRepository } from "../repositories/userRepository";
import { BooksRepository } from "../repositories/booksRepository";

export interface Repositories {
  user: typeof UserRepository
  books: typeof BooksRepository
}

const repositories: Repositories = {
  user: UserRepository,
  books: BooksRepository
}

export const apiRepositoryFactory = {
  get: (key: keyof Repositories) => repositories[key]
}

Repository plugin と axios plugin の作成

各Repositoryを呼び出すためのpluginを作成します。

import { Inject, NuxtApp } from '@nuxt/types/app'

import {
  apiRepositoryFactory,
  Repositories
} from '@/composables/factories/apiRepositoryFactory'

export default ({ app }: { app: NuxtApp }, inject: Inject) => {
  const repositories = (name: keyof Repositories) => {
    return apiRepositoryFactory.get(name)(app.$axios)
  }

  inject('repositories', repositories)
}

また、axiosの共通共通処理などを書いておきます。

// /plugins/axios.ts
import { NuxtApp } from '@nuxt/types/app'

const baseUrl = 'http://localhost:3001'

export default ({ app }: { app: NuxtApp }) => {
  app.$axios.setBaseURL(baseUrl)

  app.$axios.onRequest((config) => {
    console.log(config)
  })

  app.$axios.onResponse((config) => {
    console.log(config)
  })

  app.$axios.onError((e) => {
    console.log(e.response)
  })
}

pluginファイルを作成後、nuxt.config.js に設定を加えます。

// nuxt.config.js
export default {
  ...,

  plugins: [
    '@/plugins/repository.ts',
    '@/plugins/axios.ts'
  ],

  ...
}

サンプルページを作成し、apiを呼び出す

/pages/sample.vue を作成し、apiを呼び出してみます。

<template>
  <div>
    <h2>Users</h2>
    <ul>
      <li v-for="(user, index) in users" :key="index">
        {{ user.name }}
      </li>
    </ul>

    <h2>Books</h2>
    <ul>
      <li v-for="(book, index) in books" :key="index">
        {{ book.title }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import { defineComponent, useContext, useAsync } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const { app } = useContext()

    const users = useAsync(async () => {
      const response = await app.$repositories('user').get()
      return response.data
    })

    const books = useAsync(async () => {
      const response = await app.$repositories('books').get()
      return response.data
    })

    return {
      users,
      books,
    }
  },
})
</script>

localhost:3000/sample にアクセスすると次のような画面が表示され、各リソースのデータが取得できていることが確認できます。
sample-response-capture

これでapi呼び出しまで行えるようになりましたが、まだ問題が残っています。
現状 app.$repositories(...) の型が any となっており、app.$repositories('hogehoge') などと存在しないリソースを指定しても実行するまでエラーになりません。

return-type-any

仕上げとして最後に $repositores の型定義を行います。

型定義ファイルを作成する

/types/ ディレクトリを作成し、その中に vue.d.ts ファイルを作成します。

// /types/vue.d.ts
import { Repositories } from '@/composables/factories/apiRepositoryFactory'

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    readonly $repositories: <K extends keyof Repositories>(key: K) => ReturnType<Repositories[K]>
  }
}

定義ファイル作成後は、一旦vscodeをリロードしておきましょう。

再度sample.vueを見てみると、無事に型定義できていることがわかります。
return_type_correctly

これで完了となります。

3. 参考文献

Discussion