【json-server】json-serverのレスポンスをTypeScriptの型で縛りたい！

<!-- JSONserver レスポンスをtypescriptの型で縛りたい！ -->

# はじめに

今回は、json-serverのレスポンスをtypescriptの型で縛って型安全なモックサーバーを作成する方法を紹介します。

## 今回の構成・できること

今回紹介するjson-serverのカスタマイズでは、1つのエンドポイントに対して１つのtsファイルを作成することでレスポンスの型を縛りつつモックデータの管理をやりやすくします。

ディレクトリ構成は以下の通りです。

```
.
├── db
│   ├── user.ts
│   ├── posts.ts
│   └── comments.ts
├── index.js
└── routes.json
```

- `db` ディレクトリにはエンドポイントのレスポンスごとにtsファイルを作成
- `index.js` はjson-serverのエントリーポイント
- `routes.json` はjson-serverのルーティング設定ファイル

## json-serverのバージョン

注意として、json-serverのバージョンは最新を用いません。
バージョンは`0.17.4`を使用します。

なぜかというと、最新のバージョンでは`routes.json`を用いたルーティング設定ができなくなっているためです。

https://github.com/typicode/json-server/issues/1512

以下のコマンドでインストールします。

```bash
$ npm install json-server@0.17.4
```

## 実装

- `index.js`の実装

```javascript:./index.js
const fs = require('fs');
const ts = require('typescript');

const path = require('path');
const jsonServer = require('json-server');

// サーバーの初期化
const server = jsonServer.create();
const middlewares = jsonServer.defaults();

const routesFilePath = path.join(__dirname, '/routes.json');
const mockDir = path.join(__dirname, 'db');

const db = {};
const routes = JSON.parse(fs.readFileSync(routesFilePath, 'utf-8'));

// モックデータを読み込む
function loadMockData() {
  fs.readdirSync(mockDir).forEach((file) => {
    if (file.endsWith('.ts')) {
      const filePath = path.join(mockDir, file);

      // tsファイルを読み込む
      const tsContent = fs.readFileSync(filePath, 'utf-8');

      // jsにトランスパイル
      const jsContent = ts.transpile(tsContent);

      // 仮想的な`exports`オブジェクトを作成して評価
      const exports = {};
      const moduleWrapper = new Function(
        'exports',
        'require',
        'module',
        '__filename',
        '__dirname',
        jsContent,
      );

      moduleWrapper(exports, require, { exports }, filePath, mockDir);

      // exports.defaultを取得、dbにマージ
      if (exports.default && typeof exports.default === 'object') {
        Object.assign(db, exports.default);
      }
    }
  });
  console.log('json-server: loaded mock data');
}

// 初期ロード
loadMockData();

// ルータを初期化
const router = jsonServer.router(db);

server.use((req, res, next) => {
  // すべてのリクエストをGETと解釈させる
  req.method = 'GET';

  // クエリパラメータを削除
  req.url = req.url.split('?')[0];

  // CORS対応
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Headers', '*');

  res.on('finish', () => {
    const log = `json-server:  ${req.method} ${req.originalUrl} ${res.statusCode}`;
    console.log(log);
  });
  next();
});

// ルーティングの読み込み
server.use(jsonServer.rewriter(routes));

// ミドルウェアの適用
server.use(middlewares);

// ルータを適用
server.use(router);

// サーバー起動
server.listen(3001, () => {
  console.log('json-server: running on server !');
});
```

要点をかいつまんで解説します。

本来、`db.json`にモックデータを記述していた部分を、`db`ディレクトリに配置されたtsファイルから読み込むように変更しています。

tsファイルの内容については後述しますが、`export default`でオブジェクトをエクスポートし、それを読み取りdbオブジェクトに結合するというのを`/bd`ディレクトリ内のtsファイルに対して行っています。

```javascript:./index.jsの一部を抜粋
server.use((req, res, next) => {
  // すべてのリクエストをGETと解釈させる
  req.method = 'GET';

  // クエリパラメータを削除
  req.url = req.url.split('?')[0];

  // CORS対応
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Headers', '*');

  res.on('finish', () => {
    const log = `json-server:  ${req.method} ${req.originalUrl} ${res.statusCode}`;
    console.log(log);
  });
  next();
});
```

`req.method = 'GET';`は、全てのリクエストをGETリクエストとして扱うように設定しています。
これは、json-serverの仕様上、POSTリクエストなどを受け付けてしまうとモックデータの更新ができてしまうためです。

`req.url = req.url.split('?')[0]`は、クエリパラメータを削除しています。
これは、クエリパラメータを含めたリクエストを受け付けると、場合によってはモックデータの取得ができなくなるためです。
モックサーバーとして、ただ静的なデータを返したいのでこういった処理を行っています。

`res.header('Access-Control-Allow-Origin', 'http://localhost:3000');`は、CORS対応のための設定です。
`http://localhost:3000`は、クライアント側のURLに合わせて変更してください。

``const log = `json-server:  ${req.method} ${req.originalUrl} ${res.statusCode}`;``は、リクエストのログを出力しています。
後述する`routes.json`でリクエストパスとレスポンスパスのマッピングを行っていて、フロントエンド側からどのエンドポイントにリクエストを投げたかわからなくなるのでオリジナルのリクエストパスを出力しています。

- `routes.json`の実装

```json:./routes.json
{
  "/api/*": "/$1",
  "/users": "/users",
  "/posts": "/posts",
  "/posts/:id": "/postsDetail",
}
```

`routes.json`は、リクエストパスとレスポンスパスのマッピングを記述します。
例えば、`/api/users`にリクエストが来た場合、`/users`にリクエストを転送するように設定しています。

また、`/posts/:id`のようにパスパラメータを含む場合も記述できます。

- `db`ディレクトリの実装

`db`ディレクトリには、エンドポイントごとにtsファイルを作成します。

例えば、`/users`エンドポイントのレスポンスを縛る場合、`user.ts`を作成します。

```typescript:./db/user.ts
import { UserResponse } from '../types';

const users: { [key: string]: UserResponse } = {
  users: {
    users: [
      { id: 1, name: 'user1' },
      { id: 2, name: 'user2' },
      { id: 3, name: 'user3' },
    ],
  },
};

export default users;
```

少しわかりづらいのですが、こちらが`db/user.ts`の内容です。

`UserResponse`が本来のBEから返ってくるレスポンスの型を表しています。

`[key: string]: UserResponse`の型定義について、`[key: string]`がルーティングパスを表していて、そのパスに対して`UserResponse`型のレスポンスを返すようにしています。

`db`ディレクトリ内のtsファイルでexportされているオブジェクトは、そのオブジェクトの第一階層がroutes.jsonで指定したパスと一致している必要があります。

このusersレスポンスが返される順序としては以下のようになります。

1. FEから`http://localhost:3001/users`にGETリクエストが飛ぶ
2. `routes.json`によって`/users`にリクエストが転送される("/users": "/users")
3. `db/user.ts`でexportされたオブジェクトをもつ`index.js`の`db`オブジェクトから`users`オブジェクトを取得してレスポンスとして返す

これが今回紹介する型安全なjson-serverの仕組みです。

## サーバーの起動

```bash
$ node index.js
```

これで、json-serverが起動します。

## 改善点

ここまで紹介してきた型安全なjson-serverの仕組みですが、以下のような改善点があります。

- 同一エンドポイントで異なるHTTPメソッドの場合に対応できていない
- パスパラメータの型を縛ることができていない
- routes.jsonの記述が煩雑かつわかりづらい
- tsファイル内でのエンドポイントの指定がわかりづらい
- tsファイル内のエンドポイントの指定が他と被ってしまってもエラーが出ない

これらの改善点については、何か良い案がある方はぜひコメントで教えていただきたいです🙇‍♂️

## おわりに

最後まで読んでいただきありがとうございました！
正直完成度はあまり高くないですが、レスポンスをTypeScriptの型で縛るというメリットはかなり大きいと思いますので、なにかの参考になれば幸いです！