フロントエンドMock APIを静的型付け

agreedを利用してモックAPIを実装する備忘録として

## 使用ライブラリ

### agreed

https://github.com/recruit-tech/agreed

フロントエンド側モックサーバ 兼 バックエンド側テストクライアントとしても使える

### agreed-typed

https://github.com/akito0107/agreed-typed

agreedにtypescriptによる静的型付けを提供できる
API定義からjson形式のswaggerファイルを生成できる


## 実装

### インストール

フロント側プロジェクトにローカルインストール

```
$ npm i -D agreed agreed-typed
```

### agreed

`contract`と呼ばれるagreedにおけるクライアント要求ファイルを作成する

```:agreed.js
module.exports = [
  {
    request: {
      path: '/user/:id',
      method: 'GET',
      query: {
        q: '{:someQueryStrings}'
      },
      values: {
        id: 'yosuke',
        someQueryStrings: 'foo'
      }
    },
    response: {
      body: {
        message: '{:greeting} {:id} {:someQueryStrings}',
        images: '{:images}',
        themes: '{:themes}'
      },
      values: {
        greeting: 'hello',
        images: ['http://example.com/foo.jpg', 'http://example.com/bar.jpg'],
        themes: {
          name: 'green'
        }
      }
    }
  }
]
```

req/resにおけるqueryやbodyに記述する`{:foo}` と valuesの`foo`が紐づく

package.jsonにモックAPIサーバ起動タスクを用意するのが一般的かも

```:package.json
{
  (snip)
  "scripts": {
    "agreed": "agreed-server --path ./agreed.js --port 3010"
  }
}
```

このように手軽にフロント側でモックAPIサーバを用意できるが、この上API定義に静的型付けや必須/非必須などの情報を追加定義したい


### agreed-typed

+ typescriptによるAPI定義の静的型付け
+ swaggerジェネレータとしての機能も提供

```:agreed.ts
import {
  APIDef,
  GET,
  Capture,
  ResponseDef,
  Success200,
  Error404,
  convert,
} from 'agreed-typed';

/**
 * @summary User Get API
 */
export type UserApi = APIDef<
  GET, // HTTP Method
  ['user', Capture<':id'>], // API Path
  {}, // request header
  { q: string }, // request query
  undefined, // request body
  {}, // response header
  | ResponseDef<Success200, CreateResponseBody>
  | ResponseDef<Error404, { error: 'error test' }> // response body
>;

type CreateResponseBody = {
  message: string;
  images?: string[];
  themes?: object;
};

const User: UserApi[] = [
  {
    request: {
      path: ['user', ':id'],
      method: 'GET',
      query: {
        q: '{:someQueryStrings}',
      },
      body: undefined,
    },
    response: {
      status: 200,
      body: {
        message: '{:greeting} {:id} {:someQueryStrings}',
        images: '{:images}',
        themes: '{:themes}',
      },
      values: {
        greeting: 'hello',
        images: ['http://example.com/foo.jpg', 'http://example.com/bar.jpg'],
        themes: {
          name: 'green',
        },
      },
    },
  },
  {
    request: {
      path: ['user', '9999'],
      method: 'GET',
      query: {
        q: '{:someQueryStrings}',
      },
      body: undefined,
    },
    response: {
      status: 404,
      body: {
        error: 'error test',
      },
    },
  },
];

const agrees = [User].map((a: any) => convert(...a));
module.exports = agrees.reduce((acc, val) => acc.concat(val), []);
```

#### contractのモジュール化

contractをモジュール管理することもできるのでメンテナンス性を担保できる

```api/sendMessage.ts 
import {
  APIDef,
  POST,
  ResponseDef,
  Success200,
  Error404,
} from 'agreed-typed';

/**
 * @summary Send Message API
 */
export type SendMessageApi = APIDef<
  POST, // HTTP Method
  ['send'], // API Path
  {}, // request header
  {}, // request query
  CreateRequestBody, // request body
  {}, // response header
  | ResponseDef<Success200, CreateResponseBody>
  | ResponseDef<Error404, { error: 'error post test' }> // response body
>;

type CreateRequestBody = {
  message: string;
};

type CreateResponseBody = {
  result: string;
};

const SendMessage: SendMessageApi[] = [
  {
    request: {
      path: ['send'],
      method: 'POST',
      body: {
        message: 'foo bar',
      },
    },
    response: {
      status: 200,
      body: {
        result: 'SEND SUCCESS',
      },
    },
  },
  {
    request: {
      path: ['send'],
      method: 'POST',
      body: {
        message: 'error',
      },
    },
    response: {
      status: 404,
      body: {
        error: 'error post test',
      },
    },
  },
];

module.exports = SendMessage;
```

先に作ったuser情報取得APIも同様にモジュール化して、agreed.tsでまとめるように書き換える

```:agreed.ts
import { convert } from 'agreed-typed';

import * as GetUser from './api/getUser';
import * as SendMessage from './api/sendMessage';

const agrees = [GetUser, SendMessage].map((a: any) => convert(...a));
module.exports = agrees.reduce((acc, val) => acc.concat(val), []);
```

---
備忘録のため一旦ここまで