はじめに
今回は、json-serverのレスポンスをtypescriptの型で縛って型安全なモックサーバーを作成する方法を紹介します。
今回の構成・できること
今回紹介するjson-serverのカスタマイズでは、1つのエンドポイントに対して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
を用いたルーティング設定ができなくなっているためです。
以下のコマンドでインストールします。
$ npm install json-server@0.17.4
実装
-
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ファイルに対して行っています。
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
の実装
{
"/api/*": "/$1",
"/users": "/users",
"/posts": "/posts",
"/posts/:id": "/postsDetail",
}
routes.json
は、リクエストパスとレスポンスパスのマッピングを記述します。
例えば、/api/users
にリクエストが来た場合、/users
にリクエストを転送するように設定しています。
また、/posts/:id
のようにパスパラメータを含む場合も記述できます。
-
db
ディレクトリの実装
db
ディレクトリには、エンドポイントごとにtsファイルを作成します。
例えば、/users
エンドポイントのレスポンスを縛る場合、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レスポンスが返される順序としては以下のようになります。
- FEから
http://localhost:3001/users
にGETリクエストが飛ぶ -
routes.json
によって/users
にリクエストが転送される("/users": "/users") -
db/user.ts
でexportされたオブジェクトをもつindex.js
のdb
オブジェクトからusers
オブジェクトを取得してレスポンスとして返す
これが今回紹介する型安全なjson-serverの仕組みです。
サーバーの起動
$ node index.js
これで、json-serverが起動します。
改善点
ここまで紹介してきた型安全なjson-serverの仕組みですが、以下のような改善点があります。
- 同一エンドポイントで異なるHTTPメソッドの場合に対応できていない
- パスパラメータの型を縛ることができていない
- routes.jsonの記述が煩雑かつわかりづらい
- tsファイル内でのエンドポイントの指定がわかりづらい
- tsファイル内のエンドポイントの指定が他と被ってしまってもエラーが出ない
これらの改善点については、何か良い案がある方はぜひコメントで教えていただきたいです🙇♂️
おわりに
最後まで読んでいただきありがとうございました!
正直完成度はあまり高くないですが、レスポンスをTypeScriptの型で縛るというメリットはかなり大きいと思いますので、なにかの参考になれば幸いです!