Edited at

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


axios-mock-serverとは

フロントエンドの開発体験を向上させ、生産性を格段に高めるために作られたTypeScript製のモックサーバーです。

axios専用ではあるものの、JSON Serverよりも遥かに手軽にRESTfulなモック環境を構築出来ます。


  • GET/POST/PUT/DELETEのAPIエンドポイントを数行で作成できる

  • サーバーを立てないので静的なJSファイルとしてSPA上でも動かせる

  • TypeScript対応

  • Node環境のaxiosでも動く

  • Nuxt.js同様のオートルーティング機能でパス記述が不要

axios-mock-server - GitHub

この記事はやたら長いので以下の入門記事から読むのをおススメします。

秒でaxiosをモックするnpmモジュールの入門サンプル【ES6/JavaScript編】

Nuxt.jsのaxiosを秒でモックするnpmモジュールの入門サンプル


開発の背景

JS製のモックとしてデファクトスタンダードっぽいJSON Serverが使いづらいなーと思ったのがきっかけ。

GETだけなら簡単なんだけど、POSTやDELETEをデータに反映させようとするとExpressサーバー丸ごと書くのと同じような作業が必要で気軽には扱えない。

そこで、昨年末くらいから私が設計したプロジェクトそれぞれにカスタムして組み込んでいたaxios専用のモックサーバーをOSSとして公開しました。

axios-mock-adapterNuxt.jsから実装のヒントを得ています。


使い方 (JavaScript + ES6 modules)


インストール

$ 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クエリパラメータ(>= v0.10.0)、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/$route.js が生成されます。

以下の方法で axios の接続先がモックサーバーに変わります。


src/index.js

import axios from 'axios'

import mockServer from 'axios-mock-server'
import route from '../mocks/$route'

mockServer(route)

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 mockServer from 'axios-mock-server'
import route from '../mocks/$route'

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

mockServer(route, 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/$route.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 mockServer from 'axios-mock-server'

import route from '~/mocks/$route'

export default ({ app }) => {
mockServer(route, app.$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.8.1",
"nuxt": "^2.0.0"
}
}


レスポンス時間を遅延

デフォルト設定だとレスポンスは非同期ではあるものの即座に返されます。

ネットワークの遅延をシミュレートしたい場合は setDelayTime を使います。


src/index.js

import axios from 'axios'

import mockServer from 'axios-mock-server'
import route from '../mocks/$route'

mockServer(route).setDelayTime(500) // ms

console.time()

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



リクエストログを出力 (>= v0.10.0)

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


src/index.js

import axios from 'axios'

import mockServer from 'axios-mock-server'
import route from '../mocks/$route'

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

const mock = mockServer(route, client).enableLog()

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


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


src/index.js

mock.disableLog()



使い方 (TypeScript + ES6 modules)

Nodeプロジェクトのルートに .mockserverrc という名前の設定ファイルを作成。

outputExtでビルド時に生成される mocks/$route.js の拡張子を ts に変更します。


.mockserverrc

{

"outputExt": "ts"
}

非同期で値を返す場合、型の不一致で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 mockServer from 'axios-mock-server'
import route from '../mocks/$route'
import { User } from '../mocks/v1/users/_userId'

mockServer(route)

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


v0.10.0から、返り値を連想配列 { 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)

設定ファイルで targetcjs に変更し、CommonJS形式でexportします。(デフォルトは es6


.mockserverrc

{

"target": "cjs"
}


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 mockServer = require('axios-mock-server')
const route = require('../mocks/$route')

mockServer(route)

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



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

API定義のスクリプトファイルを置いておくディレクトリをデフォルトの mocks から変えることが出来ます。

設定ファイルの input という項目にディレクトリの相対パスを指定できます。


.mockserverrc

{

"input": "server/api"
}

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


.mockserverrc

{

"input": ["server/api1", "server/api2"]
}


src/index.js

import axios from 'axios'

import mockServer from 'axios-mock-server'
import route1 from '../server/api1/$route'
import route2 from '../server/api2/$route'

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

mockServer(route1, client1).enableLog()
mockServer(route2, 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