Azure Static Web Appsで表裏TypeScriptなNuxt.js(SSG)+NestJSのサイトを無料で公開する

[Azure Static Web Apps](https://azure.microsoft.com/ja-jp/services/app-service/static/) とは、静的サイトのホスティングとAzure Functions(AzureのFaaS)が組み合わさったサービスです。
コードはGitHubレポジトリに置き、GitHub Actionsを使ってビルドとデプロイを行います。
2021年3月現在にはプレビュー段階にあり、無料で試すことができます。（正式版リリース後もAzure Functionsと同様に無料枠内で利用できるかも？）

詳細は以下を確認してください。
- [Azure Static Web Apps のプレビューとは](https://docs.microsoft.com/ja-jp/azure/static-web-apps/overview)

この記事では、Azure Static Web Appsを使ってフロント・サーバーともにTypeScriptなNuxt.js+NestJSのサイトを公開する方法を紹介します。

# せっかちな人向け

以下のGitHubレポジトリに最小限の構成のテンプレートを公開していますので、そこから編集していってください。
[kniwase/static-web-apps-sample](https://github.com/kniwase/static-web-apps-sample)

# 前提条件
- Azure Functionsを利用できるAzureのサブスクリプションを持っていること
    - もしなければ[ここ](https://azure.microsoft.com/ja-jp/free/)で作成できます
- TypeScript, NPM, Nuxt.js, NestJSの初歩的な知識があること

# 使うもの
- [Node.js LTS](httpshttps://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions://nodejs.org/ja/)
- [nest-cli](https://github.com/nestjs/nest-cli)
- [VS Code](https://code.visualstudio.com/)
- [VS Code の Azure Functions拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions)
- [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)

# 環境構築

Azure FunctionsでNode.js用の開発をしたことがある方は、おそらく全てセットアップ済みかと思いますので、この項目は読み飛ばしてください。

### Node.js LTS
もしなければ、[公式サイト](https://nodejs.org/ja/) にある方法に従ってLTS(v14)をインストールしてください。

### nest-cli
NestJSのプロジェクト作成や起動、パッケージ管理に使用します。
もしなければ、[GitHubレポジトリ](https://github.com/nestjs/nest-cli)の説明に従ってインストールしてください。
（たぶん `npm install -g @nestjs/cli` で入ります）

### VS Code
もしインストールされていなければ、[公式サイト](https://code.visualstudio.com/)にある方法に従ってインストールしてください。

### Azure Functions拡張機能
もしインストールされていなければ、拡張機能の検索バーで `Azure Functions` を検索し、一番それっぽいやつをインストールしてください。
もしくは[こちら](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions)から拡張機能へのリンクを開いてください。

### Azure Functions Core Tools
もしインストールされていなければ、[GitHubレポジトリ](https://github.com/Azure/azure-functions-core-tools)にある方法に従ってインストールしてください。

# プロジェクト骨格の構築

以下のGitHubレポジトリで公開しているサンプルと同じものを構築していきます。
[kniwase/static-web-apps-sample](https://github.com/kniwase/static-web-apps-sample)
"setup-project"というブランチに上記サンプルを作成する過程のコミットがありますので、そちらもご参照ください。
[Commits on "setup-project"](https://github.com/kniwase/static-web-apps-sample/commits/setup-project)

### おおまかな構成

Nuxt.js のプロジェクトを先に作成し、その中に api というプロジェクト名で NestJS のプロジェクトを作成します。
パッケージはそれぞれで管理します。

## Nuxt.jsプロジェクトの作成

`static-web-apps-sample` という名前でNuxt.jsのプロジェクトを作成します。

```bash
npm init nuxt-app static-web-apps-sample
```
```:オプション
✨  Generating Nuxt.js project in static-web-apps-sample
? Project name: static-web-apps-sample
? Programming language: TypeScript
? Package manager: Npm
? UI framework: 今回は使用しない
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: 今回は何も使用しない
? Testing framework: 今回は使用しない
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: 今回は何も使用しない
? What is your GitHub username? Gitのユーザー名
? Version control system: Git
```

### nuxt-property-decorator の導入

TypeScriptでの開発をしやすくするために [nuxt-property-decorator](https://github.com/nuxt-community/nuxt-property-decorator) を導入します。

```bash:
npm i nuxt-property-decorator
```

### API側コードの除外

ビルド時にAPI側のコードを無視するように設定ます。
`tsconfig.json` の `"exclude"` に `"api"` を追加します。

```json:tsconfig.json(抜粋)
  "exclude": [
    "node_modules",
    ".nuxt",
    "dist",
    "api"
  ]
```

### ローカル起動時のAPIへのプロキシ設定

ローカルで起動した際に本番と同じパスでAPIにアクセスできるようにプロキシを設定ます。
まず [proxy-module](https://github.com/nuxt-community/proxy-module) を導入します。

```
npm i @nuxtjs/proxy
```

続いてプロキシの設定を追加します。

```javascript:nuxt.config.js(抜粋)
  # '@nuxtjs/proxy' を追加
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/proxy' 
  ],
```

```javascript:nuxt.config.js(抜粋)
  # proxy を追加
  proxy: {
    '/api': { target: 'http://localhost:7071' }
  },
```

### デプロイ用の設定

Azure Static Web Apps で設定される Github Actions でのビルドでは `npm run build` が実行されます。
しかしこのままでは Nuxt.js の静的サイト生成は行われないため、 `npm run build` で実行されるコマンドを静的サイト生成のものに変更する必要があります。
また、先ほど設定したローカル用プロキシを無効化する必要があるため、 `package.json` 内の `"build"` を `"cross-env API_URL=/ nuxt generate"` に書き換えます。


`cross-env API_URL=/` は axios が本番環境で相対パスを参照するように設定し、
`nuxt generate` はビルド時にSSGとして動作するように設定します。

```json:package.json(抜粋)
"scripts": {
    "dev": "nuxt",
    "build": "cross-env API_URL=/ nuxt generate",
    "start": "nuxt start",
    "generate": "nuxt generate"
}
```

[cross-env](https://github.com/kentcdodds/cross-env) が必要なので併せて導入します。

```bash:
npm i --save-dev cross-env
```



## NestJSプロジェクトの作成

先ほど作成したNuxt.jsのプロジェクトの中に `api` という名前でNestJSのプロジェクトを作成します。
Gitのフォルダーが作成されますが、こちらのものは削除します。

```bash:
cd static-web-apps-sample
nest new api -p npm
rm -rf api/.git
```

### Azure Functions用に設定

Azure Functions対応には [nestjs/azure-func-http](https://github.com/nestjs/azure-func-http) を使用します。
以下のコマンドでAzure用のファイルがいくつか追加されます。

```bash
cd static-web-apps-sample/api
nest add @nestjs/azure-func-http
```

ここでVSCodeを一旦閉じて、再度 `static-web-apps-sample` を開いてください。
そうすると、
`Detected an Azure Functions Project in folder "static-web-apps-sample" that may have been created outside of VS Code. Initialize for optimal use with VS Code?`
と右下に表示されるので、Yesを選択します。
これで .vscode の中に Azure Functions 用の設定が作成され、F5キーでデバッグを起動するなどの機能が使用できるようになります。

# テスト用コードの追加

ここまでで基本的な骨格の作成は完了です。
続いてAPIにアクセスしてメッセージを取得、表示するだけの簡単なコードを追加していきます。
この工程はスキップしたり別のよりちゃんとしたコードを用意しても構いません。

## NestJS

`api/src/app.service.ts` と `api/src/app.controller.ts` を以下のように書き換えます。

```typescript:api/src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): object {
    return { text: 'Hello World!' };
  }
}
```
```typescript:api/src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Get("hello")
  getHello(): object {
    return this.appService.getHello();
  }
}
```

F5キーでAzure Functionsのデバックを起動してから http://localhost:7071/api/hello にアクセスし、以下のJSONが取得できれば成功です。

```json
{"message":"Hello World!"}
```

## Nuxt.js

`components` に `HelloFromApi.vue` を追加し、下記の内容にします。

```vue:components/HelloFromApi.vue
<template>
  <div>
    Message From Api: <strong>{{ message }}</strong>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import axios from "axios";

@Component
export default class HelloFromApi extends Vue {
  message: string = "";
  async mounted() {
    this.message = await this.getMessage();
  }
  async getMessage(): Promise<string> {
    try {
      const res = await axios.get("/api/hello");
      const message: string = res.data.text;
      console.log(message);
      return message;
    } catch (error) {
      console.error(error);
      return "";
    }
  }
}
</script>
```

`pages/index.vue` を編集し、下記の内容にします。

```vue:pages/index.vue
<template>
  <div>
    <HelloFromApi />
  </div>
</template>
```

# ローカルで動作検証

ローカルで起動し、動作検証を行います。

### Nuxt.js の起動

通常通りのコマンドで起動します。

```
npm run dev
```

### NestJS の起動

F5キーを押し、Azure Functionsの拡張機能で起動します。

### 確認

http://localhost:3000/ にアクセスし、Message From Api: Hello World! と表示されれば成功です。

# GitHub にコードを上げる

ここまで編集してきたコードをまとめてコミットします。

```bash:
git checkout -b main
git add --all
git commit -m "init project"
```

続いて Github で static-web-apps-sample という名前でレポジトリを作成します。
プライベートリポジトリで問題ありません。

作成したリモートレポジトリにローカルレポジトリの内容をプッシュします。

```bash:
git remote add origin git@github.com:[ユーザー名]/static-web-apps-sample.git
git push -u origin main
```

# Azure Static Web Apps の設定

[Azure Portal](https://portal.azure.com/#home) で静的 Web アプリを開き、追加をクリックします。
設定画面が出てきますので、下記のように設定ます。

#### プロジェクトの詳細
- サブスクリプション
    - 従量課金
- リソーグループ
    - 任意のものを選ぶ、なければデフォルト設定で新規作成する

#### 静的 Web アプリの詳細
- 名前
    - static-web-apps-sample
- 地域
    - East Asia

#### デプロイの詳細

（先ほどレポジトリを作成したGitHub アカウントでサインイン）

- 組織
    - 先ほどレポジトリを作成したユーザーまたは組織
- レポジトリ
    - static-web-apps-sample
- 分岐(ブランチのこと)
    - main

#### ビルドの詳細

- ビルドのプリセット
    - Vue.js
- アプリの場所
    - /
- APIの場所
    - api
- 出力先
    - dist

設定が完了したら、確認および作成、続けて作成をクリック。

# 動作確認

新規作成されたリソースに移動後、GitHub アクションの実行 をクリックし、GitHub Actionsの実行ログを確認します。
ビルドとデプロイが完了したら、自動生成された URL にアクセスします。
ローカル実行時と同じページが見えることが確認出来たら完了です。