MERNフルスタックアプリをHerokuにデプロイする方法

# はじめに
先日、MERNフルスタックのアプリケーションをローカルで開発したので、`Heroku`にデプロイしようと思ったら、意外と参照する記事が少なかったので、備忘録的に記事にしようかと思います。

# Herokuを選択した理由
HerokuはPaasの1つで**無料でデプロイ**できるので選択しました。
AWSでもできますが、無料期間を過ぎたため、コストがかかるので、それは避けたいと思ったためです。

# デプロイ
まずはMERNプロジェクトを編集します。
私の場合、プロジェクトの階層が以下のようになっています。

```
app
 |
 |_ _ client
 |       |_ src
 |
 |_ _ server
 |       |_ src
 |
 |_ _ package.json
 |_ _ Procfile
```

後ほど、説明しますが、`package.json`と`Procfile`はHerokuのデプロイで必須となるので、作成しておきます。

## サーバー側の編集

:::note info
npm run buildしたものをサーバーで読み込む
:::
デプロイではクライアント側で生成する`build`フォルダをサーバーにインポートする必要があるので、そのための記述を加えます。

```typescript:index.ts
const path = require("path");

const app: express.Express = express();

app.use(express.static(path.resolve(__dirname, "../client/build")));

app.get("*", (request, response) => {
	response.sendFile(path.resolve(__dirname, "../client/build", "index.html"));
});
```
上記を記述することで、アプリケーションがデプロイされた後、React Routerで定義されたルーティングにリクエストを送ることができます。

:::note info
ポートの設定
:::

次にポートの設定を行います。
```typescript:index.ts
const app: express.Express = express();

// Express FWによるサーバーの立ち上げ
const PORT = process.env.PORT || 4000;

app.listen(PORT, () => {
	console.log(`Server is running on port ${PORT}`);
});
```

上記を追加します。
Herokuの場合、`process.env.PORT`とすることでHerokuのアプリケーション立ち上げ時のポート番号を取得することができます。

ちなみに、ローカルでは4000番を使用していたので、このような記述になっています。

全体の記述としては以下のようになります。
```typescript:index.ts
import express from "express";
import mongoose from "mongoose";
import "dotenv/config";
const cors = require("cors");
const path = require("path");

const app: express.Express = express();

// CORS対応
app.use(
	cors({
		origin: "*",
		exposedHeaders: [
			"Content-Length",
			"Authorization",
			"Access-Control-Allow-Origin",
			"Access-Control-Allow-Headers",
		],
	})
);

// Express FWによるローカルサーバーの立ち上げ
const PORT = process.env.PORT || 4000;

const url = process.env.MONGODB_URL as string;

// jsonオブジェクトを扱うため
app.use(express.json());

// エンドポイントからAPIを呼び出す
app.use("/api/v1", require("./src/v1/routes"));

// DB接続
try {
	mongoose.set("strictQuery", true);
	mongoose.connect(url);
	console.log("DB接続");
} catch (e) {
	console.log(e);
}

app.use(express.static(path.resolve(__dirname, "../client/build")));

app.get("*", (request, response) => {
	response.sendFile(path.resolve(__dirname, "../client/build", "index.html"));
});

app.listen(PORT, () => {
	console.log(`Server is running on port ${PORT}`);
});
```

Herokuではクロスオリジンの推奨としてワイルドカードを指定しますが、本来であれば、環境変数などで定義する必要があります。

以上が、サーバー側の設定です。

## クライアント側の編集

こちらは記述を1行変更するだけで完了します。

:::note info
BaseURLの変更
:::

```.env
REACT_APP_API_BASE_URL='https://<app-name>.herokuapp.com/<自身のAPIエンドポイント>'
```

```typescript:axiosClient.ts
import axios from "axios";

const BASE_URL = process.env.REACT_APP_API_BASE_URL as string;

const axiosClient = axios.create({
	// エンドポイントとなるURLのベース
	baseURL: BASE_URL,
});

axiosClient.interceptors.response.use(
	(response) => {
		return response;
	},
	(err) => {
		throw err.response;
	}
);

export default axiosClient;
```

APIのエンドポイントとなる`BASE_URL`をHerokuのものにするだけで、クライアント側の修正は終了です。

## package.json
次に、先ほど少し登場したプロジェクトの**ルートディレクトリ**に作成した`package.json`の中身を作成していきます。

内容としては、サーバー側のpackage.jsonとほとんど同じ大丈夫です。

```json:package.json
{
 　 // scriptsより前の記述はサーバー側のpackage.jsonをコピー
  "scripts": {
    "start": "node_modules/.bin/ts-node server/index.ts",
    "start:dev": "nodemon --exec node_modules/.bin/ts-node server/index.ts",
    "heroku-postbuild": "npm install --only=dev --no-shrinkwrap && npm install --prefix client && npm run build --prefix client"
  }
  // devDependenciesやdependencies等もサーバー側のpackage.jsonをコピー
}
```

scriptsの各内容を説明すると、以下のようになります。

- start・・・node_modulesのts-nodeを使用しindex.tsを起動（devDependenciesでts-nodeを記載しているため）
- start:dev・・・開発環境用の起動コマンド。Herokuでnodemonの起動をするとエラーが出力され、起動しない場合があるため（npm run start:dev で実行）
- heroku-postbuild・・・Herokuのデプロイで使用するスクリプト。

`heroku-postbuild`の細かい内容は以下になります。

### heroku-postbuildのコマンド内容
- `npm install --only=dev --no-shrinkwrap`・・・開発環境(`devDependencies`)のみの依存関係をインストールする。`--no-shrinkwrap`オプションは`npm-shrinkwrap.json`を無視して、`package.json`から直接依存関係を読み取るように指示する
- `npm install --prefix client`・・・clientディレクトリにある依存関係をインストールする。`--prefix`オプションは、特定のディレクトリに対して、操作を実行するために使用する。
- `nnpm run build --prefix client`・・・clientディレクトrのbuildスクリプト(`npm run build`)を実行する


## Procfile
Procfileというのは、Herokuアプリを起動時に実行したいコマンドやタスク、Webサーバー等を定義するためのファイルです。
詳しくは以下、公式を参照ください。

>[Procfile](https://devcenter.heroku.com/ja/articles/procfile#:~:text=Procfile%20%E3%81%AF%E3%80%81%E5%B8%B8%E3%81%AB%E3%80%81%20Procfile%20%E2%80%8B,%E3%81%A6%E3%81%84%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)

内容ですが、1行のみで、アプリを起動するコマンドを記載します。
```:Procfile
web: npm run start
```
上記を記述することでHerokuのデプロイプロセスが完了した際に、アプリケーションが起動します。

以上で、プログラムの設定は終了です。


## Heroku CLI
次にターミナルでHerokuのコマンドをポチポチしていきます。
※Heroku CLIをインストール済という前提で進めます。

```terminal:zsh
% heroku login
```
このコマンドでHerokuのログイン画面が立ち上がるので、ユーザー情報を入力して、ログインしてください。

次にHerokuのアプリケーションを作成していきます。

```terminal:zsh
% cd your-project
% heroku create app-name
Creating ⬢ app-name... done
https://app-name.herokuapp.com/ | https://git.heroku.com/app-name.git
```

Creating・・・doneと表示されれば成功です。
表示されたURLにアプリケーションがデプロイされます。

:::note alert
環境変数を設定
※サーバーサイドプロジェクトで環境変数を設定している場合は必須
:::
私の場合、MongoDBにアクセスするURL等を環境変数としているため、以下のように設定しました。

```terminal:zsh
% heroku config:set MONGODB_URL='mongodb+srv://<username>:<password>@clusterX.xxxxx.mongodb.net/?retryWrites=true&w=majority'
% heroku config:set SECRET_KEY='xxxxxxxxxxxxxxxx'
% heroku config:set TOKEN_SECRET_KEY='xxxxxxxxxxxxxxxx'
```

このようにして設定できます。
以下のようにして、値が出力されれば、正しく設定できています。
```terminal:zsh
% heroku config:get MONGODB_URL
mongodb+srv://<username>:<password>@clusterX.xxxxx.mongodb.net/?retryWrites=true&w=majority
```

続いてここまでの変更をgitにプッシュします。
（HerokuのアプリケーションもHeroku上のmaster(main)ブランチにプッシュする形でデプロイするため)
そして、herokuのmaster(main)ブランチにもプッシュ(デプロイ)していきます。

```terminal:zsh
% git add .
% git commit -m "Prepare for deployment to Heroku"
% git push heroku master
```
※自身のgitでmainブランチを使用している場合は以下のようにします。

```terminal:zsh
% git push heroku main
```

以下のようにターミナルに出力されればデプロイ成功です。

```terminal:zsh
emote: Verifying deploy... done.
To https://git.heroku.com/your-app.git
   xxxxxxx..xxxxxxx  main -> main
```

以下、コマンドでアプリケーションのWebページを開いてみましょう。

```terminal:zsh
% heroku open
```

:::note warn
Application Errorとなった場合
:::
上記でデプロイ自体は完了ですが、APIのエンドポイントが違ったり、ポート周辺の記述が違うなど、デプロイ後に画面表示できず`Application Error`となる場合が多々あります。

その際は、以下のコマンドでログを出力してください。

```terminal:zsh
% heroku logs --tail
```

これで、エラーの詳細を終えると思います。

以上で、MERNフルスタックアプリケーションのHerokuへのデプロイ方法についての解説を終了です。

参考になれば幸いです。

# 参考文献

> [Heroku を使用したフルスタック MERN アプリのデプロイ](https://dev-to.translate.goog/hawacodes/deploying-a-mern-app-with-heroku-3km7?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=sc)