サブディレクトリ URL で起動する Angular の Web アプリで API 通信を実装したらハマった話＋解消手順

## 概要
Angular (+ express) + Node.js でバックエンドに API 通信をさせた時にエラーでハマった経緯と、その解消策の備忘録

### 先に結論から言うと
「Nginx の設定をミスったら、バックエンド通信に失敗するよ」と言う話。

## 経緯
Angular + express で MEAN スタックで作成した Web アプリ。
今までは普通に、開発 ➡️ サーバへデプロイ ➡️ 実機で動作確認、で問題なく動いていた。
ただし、ドメイン取得して、サーバのトップレベルで動作する Web サイトとして作っていた。

今回は、サブディレクトリでのみ動作する Web サイトとして開発。
つまり...

- `https://example.com` ▶︎ 別の Web サイト（静的 HTML ページ）
- `https://example.com/my-app` ▶︎ 今回の Angular サイト

としようとした。
実際に、Angular でアプリを開発して配置。

## 事象と解消手順


### 事象１： 画面表示でエラー発生
`https://example.com/my-app` にアクセスしたら、テキストだけ出るけど装飾系が描画されない。と言う中途半端な状態で画面表示。表示が壊れている状態。。。
もちろん、`https://example.com` は問題なく規定の Web ページが表示される。

### ▶︎ 解消： 「base href」を設定したら解消

index.html の `<base href="/">` が `<base href="/my-app/">` となると解消する。
しかし、ローカル環境では必要ない...。
なので、以下のとおり、package.json のビルドコマンドに `--base-href` オプションを指定することで、ビルド時に「base href」を変え分けられるようにした。

```json-doc:package.json
  :
"scripts": {
    // 起動コマンド
    "dev": "node ./dist/server/server.js",
    // ビルドコマンド (ng build に --base-href オプション追加)
    "build": "npm run build:client && npm run build:server",
    "build:client": "ng build --prod --base-href=/my-app/ && ng run my-app:server",
    "build:server": "tsc --project server --outDir dist --allowjs true",
    :
  },
  :
```

***

### 事象２： バックエンドへの通信時にエラー発生

「事象１」が解消し、画面表示はできるようになったが、
今度は、API でバックエンドにリスト取得する処理を実装したら以下のエラー事象が発生。

ローカルだと取得できてたのに、サーバだとリスト取得できない。。。（本来なら JSON のリストデータが返却される予定）
ブラウザコンソールを開いてみたら、以下のエラーログを吐いていた。

```bash
# 発生したエラー

error: SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse...
headers: t {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}
message: "Http failure during parsing for https://iexample.com/portal/api/my-app/api/heros"
name: "HttpErrorResponse"
ok: false
status: 200
statusText: "OK"
url: "https://example.com/my-app/api/heros"
```

バックエンド通信で、HTTP 結果コードは 200 が返っているが、返却された後に何かエラーが出ている。

`error` の部分を開いて詳しく見てみると

```console
SyntaxError: Unexpected token < in JSON at position 0 at JSON.parse (<anonymous>) at XMLHttpRequest
```

さらに、すぐ下に

```
text: "<!DOCTYPE html><html lang="ja"><head>↵  <meta charset="utf-8">↵  <title>My App</title>↵ 
```

JSON データ、もしくは、 JSON のリストデータが返却されるべき箇所で、HTML を返しているために、JSON.parse() でエラーが発生したことが原因らしい。

長く悩んだが、よく考えたらそりゃそうだ。Web サービスで画面用のルーティングしか設定してなかった。

### ▶︎ 解消： Nginx のルーティングに API 専用のルートを追加したら解消

```nginx:/etc/nginx/conf.d/default.conf
server {
    # http を https にリダイレクト
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  localhost;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # *** 追加: 以下の API 用ルーティングを追加 ↓↓↓ ******
    location /my-app/api/ {
        proxy_pass http://127.0.0.1:3000/api/;
        proxy_redirect default;
    }
    # *** 追加 ↑↑↑ *********************************

    location /my-app/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_redirect default;
    }

    location / {
        root   /var/www;
        index  index.php index.html index.htm;
    }

    # 404, 50x 等 エラー系のルートが以下にある
}
```

設定後、`nginx -s reload` で Nginx を再起動。
Angualar も再デプロイして起動し直し、ブラウザから再度アクセスしてみたら、
エラーは出なくなり、問題なく目的の JSON リストデータが返却されて、画面に表示された。


## ハマりどころ

そもそも「Angular の設定方法やロジックに原因がある」として、ずーーーーっと、アプリ設定のネット記事を漁っていたが、そこが間違いだった。

### 原因の切り分けフロー

HTTP 結果は 200 で、エラーの吐き方が、JSON パースエラー： **クラサバ通信はできている**
→ リクエストとレスポンスの機構を改めて確認： **SSR で実装しているので、リクエスト/レスポンスは HTML を返却するのと JSON データを返却する２種類がある**
→ ローカル環境で、ブラウザに GET メソッドの API を叩いてみる： **JSON データが返り、画面に表示されることを確認**
→ 公開サーバで、同じくブラウザに GET メソッドの API を叩いてみる： **トップページが再表示された ＝ HTML が返却された**
→ Web サーバのリクエスト URI に対するルーティング先を修正すべきことに気づく
→ Nginx の config を修正して動作確認
→ **問題解消 !!!**

こんな感じだった。

エラーの切り分けを丁寧に行い、対応していくことは大事だ。
以上、Web サービスの設定は、大した内容ではないが間違えると結構ハマる。