JSON Serverのおしいところをなんとかしたい

typicode/json-server: Get a full fake REST API with zero coding in less than 30 seconds (seriously)

JSON Serverの特徴

  • コードの記述なしにモックサーバをたてれる
  • JSONファイルを1つ準備すればOK
  • CRUD対応
  • クエリでいろんなフィルターやソートなどを指定ができる

こんな感じで利用できる

{
  "profile": { "name": "typicode" },
  "scores": [
    {"id":1, "name": "javascript", "score": 80},
    {"id":2, "name": "css", "score": 50}
  ]
}

起動

json-server --watch db.json

リソースパス

localhost:3000/profile
localhost:3000/scores

ソート指定

/scores?_sort=score&_order=asc
/scores?_sort=score,name&_order=desc,desc

範囲指定

/scores?score_gte=40&score_lte=70

おしいところ

分割管理・階層管理できない

{
    "aaa": {
        "bbb": []
    },
    "ccc": []
}
  • localhost:3000/aaa/bbb とはならない(設定で仮想パスを置き換えることは可能だけど結構不便)
  • /api/aaa/bbb.json 、 /api/ccc.json ていうふうにファイルを分けて管理したい

クエリ文字列が独自仕様で本番では使えない

/scores?score_gte=40&score_lte=70
  • 本番APIでは _gte とか _lte とかで範囲を指定しない

Expressを使って解決する

  • Expressが内包されてるのでそれを使う
  • JSON Server用のミドルウェア関数もある
const jsonServer = require('json-server');

// Expressをインスタンス化する
const app = jsonServer.create();

const middlewares = jsonServer.defaults();
app.use(middlewares);

const router = jsonServer.router('db.json');
app.use(router);

app.listen(3000, () => {
  console.log('JSON Server is running');
});

階層構造によるモックデータの分割管理

  • 仮想パス、物理パス共に同一のディレクトリ構造で管理したい
# 階層構造でモックデータを管理
- api/
    └ hr/
       ├ employees.json   # {"employees": [...]}
       └ attendance/
          ├ fulltime.json # {"fulltime": [...]}
          └ parttime.json # {"parttime": [...]}

# 以下のようにリソースパスを公開したい
/api/hr/employees
/api/hr/attendance/fulltime
/api/hr/attendance/parttime

各階層パスにJSONを割り当てる

  • app.useでは、ベースとなるパスを指定できる
  • router()メソッドでは、json objectを指定できる
app.use('/api/hr', jsonServer.router({
  "employees": [...]
}));
app.use('/api/hr/attendance', jsonServer.router({
  "fulltime": [...],
  "parttime": [...]
}));

ルートパスのみを指定する

  • ルートパスを起点に各階層を再帰的に探索して、階層パスと階層単位で集約したJSONを得る
const resourceCollector = require('./resource-collector');
resourceCollector(
  'api',
  (vPath, routeJson) => app.use(vPath, jsonServer.router(routeJson))
);
const path = require('path');
const fs = require('fs-extra');

const resourceCollector = (basePath, cb) => {

  // ディレクトリか?
  if (fs.statSync(basePath).isDirectory()) {

    // カレントディレクトリにあるファイルや子ディレクトリを順次読み込みする
    const routeJson = fs.readdirSync(basePath).reduce((buf, name) => {
      const currentPath = path.join(basePath, name);

      // JSONなら集約し、ディレクトリなら再帰的に自身の関数を呼ぶ
      path.extname(name) === '.json' ?
        Object.assign(buf, fs.readJsonSync(currentPath)) :
        resourceCollector(currentPath, cb);
      return buf;
    }, {});

    // 階層単位のパスと集約したJSONを返す
    const vPath = path.join('/', basePath);
    cb(vPath, routeJson);
  }
};
module.exports = resourceCollector;

クエリ名をAPI仕様に合わせて変換する

  • scoreRangeFrom -> score_gte
  • scoreRangeTo -> score_lte
app.get('/api/scores', (req, res, next) => {
  if ('scoreRangeFrom' in req.query) {
    req.query.score_gte = req.scoreRangeFrom;
  }
  if ('scoreRangeTo' in req.query) {
    req.score_lte = req.query.scoreRangeTo;
  }
  next();
});

変換ルールをAPI全体に割り当てる

const queryRewriter = require('./query-rewriter');

app.get('/api/*', queryRewriter([
  ['^(.+)RangeFrom$', '$1_gte'],
  ['^(.+)RangeTo$', '$1_lte']
]));
const queryRewriter = (patternMap) => {
  return (req, res, next) => {
    for(let name in req.query){
      patternMap.forEach(item => {
        const originalPattern = item[0];
        const defaultPattern = item[1];
        const reg = new RegExp(originalPattern);
        if (reg.test(name)) {
          const _name = name.replace(reg, defaultPattern);
          req.query[_name] = req.query[name];
        }
      });
    }
    next();
  }
};
module.exports = queryRewriter;