AWS Amplify mock apiでsubscriptionを快適に使用するためにプロキシ環境を作成したよ（NextJS）

# 経緯

* Amplify mock api は、ローカル開発を加速させることができるので非常に便利
* しかし、現状(2022年3月)時点では、[チュートリアル](https://docs.amplify.aws/cli/usage/mock/#function-mocking-with-graphql)に書かれた方法では動作しない
* AppSyncを使いたい大きな動機が、subscriptionであり、どうしてもローカルでシミュレートしたい

# 対応方法

* 結局のところ、mock apiのサーバ自体はsubscriptionに対応している
* しかし、aws-sdkが未対応のため、誤ったエンドポイントに通信しており使用できない状況
* ローカルにプロキシを立てることで、sdkからmock apiへの通信を修正すれば使用できる

# 動作検証バージョン

"aws-amplify": "^4.3.14"
"next": "^11.1.3"

# 詳細

## mock api はsubscriptionに対応していない？

元々subscriptionが動作しないというバグがあったようだが、修正されているとのことでした。
https://github.com/aws-amplify/amplify-cli/issues/6026

しかしmock apiを動作させると分かりますが、どうもクライアントからの通信先がおかしいです。
aws-sdkの修正状況にずれが生じていることが原因のようです。

issueでは、subscription の mock api 動作時に`/graphql`ではなく、`/graphql/realtime`へアクセスしてしまっていることが問題のため、`rewrites`などして向き先を変える必要があるとのことでした。
https://github.com/aws-amplify/amplify-cli/issues/9621

## URLの書き換えをするだけなのだが。。

`/graphql/realtime`へのアクセスを`/graphql`に書き換える(`/graphql`へのアクセスはそのままにする）だけで、問題は解決しそうです。

NextJSを使用しているなら、`rewrites`の設定するだけということでパッとやってみましたが、うまく動作しませんでした。また、rewriteは気軽に使用しない方がいいという記事も発見しました。
https://zenn.dev/catnose99/scraps/91c31ee6609929

そもそも、 __テスト環境作るためだけに、rewrite設定なんて入れたくない__ ため、別の手段を考えることにしました。

## nodeモジュールでプロキシを立てる

nginxなど、プロキシアプリを別に設定することも考えましたが、NextJSプロジェクトの中で(node_modulesで)、管理できると便利だと考え、以下のようにカスタマイズしてプロキシを立てることとしました。

### プロキシの方針

* [node-http-proxy](https://github.com/http-party/node-http-proxy)をベースにする
* URL書き換えのオプションがなかったのでカスタマイズする
* package.jsonにコマンド記載してyarn等で起動管理する

### node-http-proxyをカスタマイズ

カスタマイズ結果は[blueoath-video-ec/node-http-proxy](https://github.com/blueoath-video-ec/node-http-proxy)を参照。

`pathRewrite`オプションを新設して、正規表現でURL書き換えを行います。
```lib/http-proxy/common.js
  if (options.pathRewrites && (options.pathRewrites instanceof Array)) {
    for (var i = 0; i < options.pathRewrites.length; i++) {
      console.log(outgoing.path);
      var from = options.pathRewrites[i].from
      var to = options.pathRewrites[i].to
      outgoing.path = outgoing.path.replace(new RegExp(from, 'g'), to);
    }
  }
```
使い方は以下のような感じです。
```proxy.js
var proxy = new httpProxy.createProxyServer({
  ...
  pathRewrite: [
    { from: "^/graphql/realtime", to: "/graphql" }
  ],
  ...
});
```

### package.jsonへの組み込み

まずは、カスタマイズしたライブラリを追加します。
package.jsonに以下のように追加して`yarn install`を実行します。
```package.json
  "devDependencies": {
    ...
    "node-http-proxy": "git+https://github.com/blueoath-video-ec/node-http-proxy.git",
    "kill-port": "^1.6.1",
    ...
  }

```
:::note info
yarnを使用していますが、適宜npmなどに読み替えてください。
カスタマイズしたライブラリは適宜自分の環境にforkするなどしてください。
:::

プロジェクトのルートに`proxy.json`を配置して、プロキシサーバの起動コードを記載します。
<details><summary>サンプルコード</summary>

```proxy.js
// NOTE: カスタマイズしたProxyを使用していることに注意（pathRewrite追加している）
// https://github.com/http-party/node-http-proxy
// https://github.com/blueoath-video-ec/node-http-proxy
var http = require('http');
var httpProxy = require('node-http-proxy');

var mockAPIServerPort = 20002
var proxyPort = 8015

//
// Setup our server to proxy standard HTTP requests
//
var proxy = new httpProxy.createProxyServer({
  target: {
    protocol: 'http',
    host: 'localhost',
    port: mockAPIServerPort,
    path: '/',
  },
  pathRewrite: [
    { from: "^/graphql/realtime", to: "/graphql" }
  ],
  changeOrigin: true
});
var proxyServer = http.createServer(function (req, res) {
  proxy.web(req, res, function (err) {
    // Now you can get the err
    // and handle it by your self
    // if (err) throw err;
    console.log("http error === ")
    console.log(err);
  });
});
//
// Listen to the `upgrade` event and proxy the
// WebSocket requests as well.
//
proxyServer.on('upgrade', function (req, socket, head) {
  proxy.ws(req, socket, head, function (err) {
    // Now you can get the err
    // and handle it by your self
    // if (err) throw err;
    console.log("ws error === ")
    console.log(err);
    socket.close();
  })
});

console.log("mockAPIServerPort: " + mockAPIServerPort)
console.log("proxyPort: " + proxyPort)

proxyServer.listen(proxyPort);
```
:::note info
mockAPIServerPortは、mock apiサーバのポートです。
proxyPortは、今回作成したプロキシサーバのポートです。
:::
</details>

### プロキシサーバ起動オプション追加

package.jsonに以下を追記します。
```package.json
  "scripts": {
    ...
    "mock:api": "yarn kill-amplify-mock-api && amplify mock api",
    "mock:proxy": "node ./proxy.js",
    "kill-amplify-mock-api": "kill-port 20002 && kill-port 20003"
  },
```
これで`yarn mock:api`と打つと、mockサーバを停止して、プロキシサーバが起動されます。

## アプリから使うぞ！

プロキシサーバを立てても、それだけではmock apiを使用できません。
`Amplify.configure`における、`aws-exports`の情報を書き換えて、エンドポイントがプロキシサーバに向くように修正が必要です。コンフィグ設定については[ここ](https://aws-amplify.github.io/amplify-js/api/#configuration)を参照。

```somewhere-in-the-app.tsx
import awsExports from '../aws-exports'

//Amplify.configure({ ...awsExports, ssr: true })
let usingProxy = awsExports
usingProxy.aws_appsync_graphqlEndpoint = 'http://localhost:8015/graphql'
Amplify.configure({ ...usingProxy, ssr: true })
```
:::note warn
上記の設定は、 __ローカルデバッグの時のみ実施__ する必要があります。
環境変数等で、コードの有効無効を切り替えてください。
:::
:::note info
`localhost:8015`のポートは、`proxy.js`で設定した`proxyPort`の値を設定します。
なお、`amplify mock api` 実行により、aws-exports.jsの値が自動で書き変わります（mock apiの便利なところ）。
```aws-exports.js
const awsmobile = {
    ...
    "aws_appsync_graphqlEndpoint": "http://192.168.12.8:20002/graphql",
```
上記の:20002はmock api サーバのエンドポイントです。
これを
```somewhere-in-the-app.tsx
usingProxy.aws_appsync_graphqlEndpoint = 'http://localhost:8015/graphql'
```
でプロキシサーバのエンドポイントに上書き変更しています。
:::

# まとめ

AWS Amplify mock apiでsubscriptionは動作しますが、一工夫必要です。
Amplify(AppSync)は、subscriptionがあるから使ってるようなものなので、ローカル環境でmock apiが動くととても快適に開発が進められます。多少のモック環境用コードを付与する必要がありますが、やる価値はあると思いますので、ぜひお試しください。