RESTfulな「axios-mock-server」の使い方

## axios-mock-serverとは
フロントエンドの開発体験を向上させ、生産性を格段に高めるために作られたTypeScript製のモックサーバーです。
axios専用ではあるものの、JSON Serverよりも遥かに手軽にRESTfulなモック環境を構築出来ます。

- GET/POST/PUT/DELETEのAPIエンドポイントを数行で作成できる
- サーバーを立てないので静的なJSファイルに出力してブラウザ単体でも動かせる
- TypeScript対応
- Node環境のaxiosでも動く
- Nuxt.js同様のオートルーティング機能でパス記述が不要
- IE11対応(>= v0.13.1)

[axios-mock-server - GitHub](https://github.com/m-mitsuhide/axios-mock-server "axios-mock-server - GitHub")

この記事はやたら長いので以下の入門記事から読むのをおススメします。
[秒でaxiosをモックするnpmモジュールの入門サンプル](https://qiita.com/m_mitsuhide/items/e0dcd0d3e37eee59bcc2)
[Nuxt.jsのaxiosを秒でモックするnpmモジュールの入門サンプル](https://qiita.com/m_mitsuhide/items/d93d1de1a3336a603b9e)

## 開発の背景
JS製のモックとしてデファクトスタンダードっぽい[JSON Server](https://github.com/typicode/json-server)が使いづらいなーと思ったのがきっかけ。
GETだけなら簡単なんだけど、POSTやDELETEをデータに反映させようとするとExpressサーバー丸ごと書くのと同じような作業が必要で気軽には扱えない。

そこで、昨年末くらいから私が設計したプロジェクトそれぞれにカスタムして組み込んでいたaxios専用のモックサーバーをOSSとして公開しました。
[Nuxt.js](https://ja.nuxtjs.org/)のオートルーティング機能から実装のヒントを得ています。

## 使い方 (JavaScript + ES6 modules)
v0.13.1以上という前提で解説していきます。

### インストール
```bash
$ npm install --save-dev axios-mock-server
```

### API作成

Nodeプロジェクトのルートに `mocks` ディレクトリを作成します。
[Nuxt.jsのオートルーティング](https://ja.nuxtjs.org/guide/routing/)と同じ命名規則でjsファイルを作成します。
以下の例だと `/v1/users/:userId` のエンドポイントにGETとPOSTのモックAPIが作成されます。
メソッドが受け取る引数は `{ values, params, data, config }（valuesはパスのアンダースコア部分、paramsはURLクエリパラメータ、dataはPOSTなどで送信したデータ、configはaxiosのリクエストconfig）` で、返り値は `[HTTPステータス, データ, ヘッダー]` です。（必須要素はHTTPステータスのみ）
`async/await` も使用可能です。

```javascript:mocks/v1/users/_userId.js
const users = [
  { id: 0, name: 'taro' },
  { id: 1, name: 'hanako' }
]

export default {
  get({ values }) {
    return [200, users.find(user => user.id === values.userId)]
  },

  post({ data }) {
    users.push({
      id: users.length,
      name: data.name
    })

    return [201]
  }
}
```

### axiosとの接続

npmスクリプトでビルドすると `mocks/$mock.js` が生成されます。
以下の方法で `axios` の接続先がモックサーバーに変わります。

```javascript:src/index.js
import axios from 'axios'
import mock from '../mocks/$mock'

mock()

axios.get('https://google.com/v1/users/1').then((user) => {
  console.log(user) // { id: 1, name: 'hanako' }
})
```

axiosのinstanceをモックしたい場合は初期化時に引数で渡します。

```javascript:src/index.js
import axios from 'axios'
import mock from '../mocks/$mock'

const client = axios.create({ baseURL: 'https://google.com/v1' })

mock(client)

client.get('/users/1').then((user) => {
  console.log(user) // { id: 1, name: 'hanako' }
})

axios.get('https://google.com/v1/users/1').catch((e) => {
  console.log(e.response.status) // 404 (axios本体はモックされてない)
})
```

### npmスクリプト

`mocks/$mock.js` を1回ビルドするのが `-b`
ファイルが変更されるたびにビルドするのが `-w`
設定ファイルの場所を変えたい場合は `-c <file path>`

```json:package.json
{
  "scripts": {
    "mock:build": "axios-mock-server -b",
    "mock:watch": "axios-mock-server -w",
    "mock:config": "axios-mock-server -b -c settings/.mockserverrc"
  }
}
```

## NeDBで永続化
DBガッツリ使うとモックの意義が薄れるのですが、とはいえデータの変更を保持しておきたい場面もあるわけです。
そこでNeDBというJavaScriptのみで書かれたDBを紹介します。
MongoDBライクにテーブル定義や設定せずにすぐ使えるので今回のような用途にピッタリです。
静的ファイルだけのSPAでも動作するのが素晴らしい・・・

動作環境に応じて最適なデータの保存先を自動決定してくれます。
Node.jsならファイルに、ブラウザならIndexedDB、なければlocalStorageになるようです。
とはいえNeDBは非同期メソッドがコールバック方式で扱いづらいのでラッパーライブラリのNeDB-promisesを使うのが良さそうです。
（単数形のNeDB-promiseは更新が止まっているのとTSの型定義ファイルがない別物ので注意）

### NeDB-promisesインストール
```bash
$ npm install --save-dev nedb-promises
```

### NeDB-promises + JavaScript

```javascript:mocks/v1/users/_userId.js
import Datastore from 'nedb-promises'

const datastore = Datastore.create('dbname')

export default {
  async get({ values }) {
    return [200, await datastore.find({ id: values.userId })]
  },

  async post({ values, data }) {
    return [
      201,
      await datastore.insert({ id: values.userId, name: data.name })
    ]
  }
}

// 以下のように書いても等価です
// asyncData(HTTPステータス, データを返すPromise, ヘッダー)
import { asyncResponse } from 'axios-mock-server'
import Datastore from 'nedb-promises'

const datastore = Datastore.create('dbname')

export default {
  get: ({ values }) => asyncResponse(200, datastore.find({ id: values.userId })),
  post: ({ values, data }) => asyncResponse(201, datastore.insert({ id: values.userId, name: data.name }))
}
```

## multipart-formdata対応
サーバーを立てずにモックしているため、通常の方法では画像をPOSTしたあとにimgタグで表示する方法がありません。
AWS S3など外部に保存するのも手間がかかりすぎるので、ここではdataURIを使う方法を紹介します。

```javascript:mocks/v1/images/index.js
export const images = []

export default {
  post: ({ data }) => new Promise((resolve) => {
    const file = data.get('file') // FormData#get
    const reader = new FileReader()

    reader.onload = () => {
      const image = {
        id: images.length,
        url: reader.result
      }

      images.push(image)
      resolve([200, image])
    }

    reader.readAsDataURL(file)
  })
}
```
```javascript:mocks/v1/images/_imageId.js
import { images } from './index'

export default {
  get({ values }) {
    return [200, images.find(image => image.id === values.imageId)]
  }
}
```
```javascript:src/index.js
const inputElm = document.getElementsByTagName('input')[0]

inputElm.addEventListener('change', async (e) => {
  const formData = new FormData()
  formData.append('file', e.target.files[0])

  const { data: { id }} = await axios.post('/v1/images', formData, {
    headers: { 'content-type': 'multipart/form-data' }
  })

  const { data: { url }} = await axios.get(`/v1/images/${id}`)
  console.log(url) // data:image/jpg;base64,..

  const img = new Image()
  img.src = url
  document.body.appendChild(img)
}, false)
```

## @nuxtjs/axiosとの連携

[Axios Moduleのセットアップ](https://axios.nuxtjs.org/setup)が完了している前提で解説します。
[create-nuxt-app](https://ja.nuxtjs.org/guide/installation/)を使うとaxios込みでNuxt.jsのインストールがラクです。

```javascript:plugins/mock.js
import mock from '~/mocks/$mock'

export default ({ $axios }) => mock($axios)
```

```javascript:nuxt.config.js
export default {
  plugins: ['~/plugins/mock.js']
}
```

```javascript:mocks/users/_userId.js
const users = [
  { id: 0, name: 'taro' },
  { id: 1, name: 'hanako' }
]

export default {
  get({ values }) {
    return [200, users.find(user => user.id === values.userId)]
  }
}
```

```vue:pages/index.vue
<template>
  <div />
</template>

<script>
export default {
  async mounted() {
    console.log(
      await this.$axios.$get('/users/1')
    ) // { id: 1, name: 'hanako' }
  }
}
</script>
```

```json:package.json
{
  "scripts": {
    "dev": "axios-mock-server -b && nuxt",
    "build": "axios-mock-server -b && nuxt build",
    "start": "axios-mock-server -b && nuxt start",
    "generate": "axios-mock-server -b && nuxt generate"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "axios-mock-server": "^0.13.1",
    "nuxt": "^2.0.0"
  }
}
```

## レスポンス時間を遅延
デフォルト設定だとレスポンスは非同期ではあるものの即座に返されます。
ネットワークの遅延をシミュレートしたい場合は `setDelayTime` を使います。

```javascript:src/index.js
import axios from 'axios'
import mock from '../mocks/$mock'

mock().setDelayTime(500) // ms

console.time()

axios.get('/v1/users/1').then(() => {
  console.timeEnd() // default: 501.565185546875ms
})
```

## リクエストログを出力
`enableLog` を呼び出すと、コンソールにリクエストのHTTPメソッドとルート絶対パスが出力されます。

```javascript:src/index.js
import axios from 'axios'
import mock from '../mocks/$mock'

const client = axios.create({ baseURL: 'https://google.com/v1' })

const mockServer = mock(client).enableLog()

client.get('/users/1?aa=123', { params: { bb: 'hoge' }}) // [mock] get: /v1/users/1?aa=123&bb=hoge => 200
```

ログ出力を止めるには `disableLog` を呼び出します。

```javascript:src/index.js
mockServer.disableLog()
```

## 使い方 (TypeScript + ES6 modules)
非同期で値を返す場合、型の不一致でTypeScriptがエラーを吐くので `MockResponse` をアサーションしてください。

```typescript:mocks/v1/users/_userId.ts
import { MockMethods, MockResponse } from 'axios-mock-server'

export type User = {
  id: number
  name: string
}

const users: User[] = [
  { id: 0, name: 'taro' },
  { id: 1, name: 'hanako' }
]

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)

const methods: MockMethods = {
  async get({ values }) {
    await sleep(100)
    return [200, users.find(user => user.id === values.userId)] as MockResponse
  }
}

export default methods
```

```typescript:src/index.ts
import axios from 'axios'
import mock from '../mocks/$mock'
import { User } from '../mocks/v1/users/_userId'

mock()

axios.get<User>('https://google.com/v1/users/1').then((user) => {
  console.log(user) // { id: 1, name: 'hanako' }
})
```
返り値を連想配列 `{ status, data, headers }` にすることで非同期メソッドでもアサーションが不要になります。

```typescript:mocks/v1/users/_userId.ts
import { MockMethods } from 'axios-mock-server'

export type User = {
  id: number,
  name: string
}

const users: User[] = [
  { id: 0, name: 'taro' },
  { id: 1, name: 'hanako' }
]

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)

const methods: MockMethods = {
  async get({ values }) {
    await sleep(100)
    return { status: 200, data: users.find(user => user.id === values.userId) }
  }
}

export default methods
```

## 使い方 (Node.js + CommonJS modules)

```javascript:mocks/v1/users/_userId.js
const users = [
  { id: 0, name: 'taro' },
  { id: 1, name: 'hanako' }
]

module.exports = {
  get({ values }) {
    return [200, users.find(user => user.id === values.userId)]
  }
}
```
```javascript:src/index.js
const axios = require('axios')
const mock = require('../mocks/$mock')

mock()

axios.get('https://google.com/v1/users/1').then(function(user) {
  console.log(user) // { id: 1, name: 'hanako' }
})
```

## インプットディレクトリの変更

API定義のスクリプトファイルを置いておくディレクトリをデフォルトの `mocks` から変えることが出来ます。
設定ファイルを `.mockserverrc` という名前でディレクトリのルートに作成し、 `input` という項目にディレクトリの相対パスを指定できます。

```json:.mockserverrc
{
  "input": "server/api"
}
```
配列で複数ディレクトリを指定することも可能です。

```json:.mockserverrc
{
  "input": ["server/api1", "server/api2"]
}
```
```javascript:src/index.js
import axios from 'axios'
import mock1 from '../server/api1/$mock'
import mock2 from '../server/api2/$mock'

const client1 = axios.create({ baseURL: 'https://google.com/v1' })
const client2 = axios.create({ baseURL: 'https://google.com/v2' })

mock1(client1).enableLog()
mock2(client2).enableLog()

client1.get('/users/1?aa=123', { params: { bb: 'hoge' }}) // [mock] get: /v1/users/1?aa=123&bb=hoge
client2.get('/users/1?aa=123', { params: { bb: 'hoge' }}) // [mock] get: /v2/users/1?aa=123&bb=hoge
```


## まとめ
サーバーの実装を待たずにフロントが先行して開発を進めるとか、フロントチームだけでプロトタイプを作るのにも相当役立つはずです。
HTTPクライアント界隈では最近[ky](https://github.com/sindresorhus/ky)がアツい感じなのでaxiosがいつまでスタンダードであり続けるかは気になるところですが。

バグ報告や使い方の質問は気軽にいただけると嬉しいです。

[axios-mock-server - GitHub](https://github.com/m-mitsuhide/axios-mock-server "axios-mock-server - GitHub")