Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

この記事はやたら長いので以下の入門記事から読むのをおススメします。
秒でaxiosをモックするnpmモジュールの入門サンプル
Nuxt.jsのaxiosを秒でモックするnpmモジュールの入門サンプル

開発の背景

JS製のモックとしてデファクトスタンダードっぽいJSON Serverが使いづらいなーと思ったのがきっかけ。
GETだけなら簡単なんだけど、POSTやDELETEをデータに反映させようとするとExpressサーバー丸ごと書くのと同じような作業が必要で気軽には扱えない。

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

使い方 (JavaScript + ES6 modules)

v0.13.1以上という前提で解説していきます。

インストール

$ npm install --save-dev axios-mock-server

API作成

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

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 の接続先がモックサーバーに変わります。

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をモックしたい場合は初期化時に引数で渡します。

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>

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インストール

$ npm install --save-dev nedb-promises

NeDB-promises + 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を使う方法を紹介します。

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)
  })
}
mocks/v1/images/_imageId.js
import { images } from './index'

export default {
  get({ values }) {
    return [200, images.find(image => image.id === values.imageId)]
  }
}
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のセットアップが完了している前提で解説します。
create-nuxt-appを使うとaxios込みでNuxt.jsのインストールがラクです。

plugins/mock.js
import mock from '~/mocks/$mock'

export default ({ $axios }) => mock($axios)
nuxt.config.js
export default {
  plugins: ['~/plugins/mock.js']
}
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)]
  }
}
pages/index.vue
<template>
  <div />
</template>

<script>
export default {
  async mounted() {
    console.log(
      await this.$axios.$get('/users/1')
    ) // { id: 1, name: 'hanako' }
  }
}
</script>
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 を使います。

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メソッドとルート絶対パスが出力されます。

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 を呼び出します。

src/index.js
mockServer.disableLog()

使い方 (TypeScript + ES6 modules)

非同期で値を返す場合、型の不一致でTypeScriptがエラーを吐くので MockResponse をアサーションしてください。

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
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 } にすることで非同期メソッドでもアサーションが不要になります。

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)

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)]
  }
}
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 という項目にディレクトリの相対パスを指定できます。

.mockserverrc
{
  "input": "server/api"
}

配列で複数ディレクトリを指定することも可能です。

.mockserverrc
{
  "input": ["server/api1", "server/api2"]
}
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がアツい感じなのでaxiosがいつまでスタンダードであり続けるかは気になるところですが。

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

axios-mock-server - GitHub

m_mitsuhide
frourio / aspida / velona / TypeScript / Nuxt.js
https://github.com/solufa
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away