Nuxt.js と SpringBoot で始める SPA/API サーバ開発とデプロイ

Nuxt.js v2で作成するSinglePageApplication(以下、SPAと省略)とSpringBoot v2で作成するAPI サーバの組み合わせでWebアプリケーションを開発する際の環境構築、および、Netlify/Heroku へのデプロイ手順をまとめました。主に Nuxt.js v2 と SpringBoot v2 の組み合わせに焦点を当てて説明します。

## 構築の前に

以下のソフトウェアはインストール済みの前提とします。

- Node.js v10
- Java Development Kit(以下、JDK) 16
- Maven v3

また、Netlify/Heroku のアカウントは作成済みとします。

## ディレクトリ構成

「assets」ディレクトリに Nuxt.js アプリを配置、「server」ディレクトリに Maven 形式の Spring Boot アプリを配置した単一ディレクトリ構成の前提で説明します。

```text
.
├── assets
│   ├── node_modules
│   ├── nuxt.config.js
│   ├── package.json
│   ...
│
└── server
    ├── pom.xml
    ├── src
    ...
```

## 開発環境の作り方

Nuxt.js と SpringBoot でホットリローディングができる環境を作ります。

```text
                                                 +-------------+
               +------------+      /api          |             |
               |            +------------------->| Spring Boot |
               |            |   localhost:8080   |             |
               | Nuxt.js    |                    +-------------+
-------------->| Dev Server |
localhost:3000 |            |
               |            |
               +------------+
```

Webアプリケーション利用者から、localhost:3000 のみアクセスされるようにしておくと、クロスドメイン関連の対応が不要になります。この構成のポイントは 2 点です。

- /api へのリクエストは Nuxt.js Dev Server が localhost:8080 で動作している SpringBoot に転送(Proxy)し、SpringBoot が処理する。
- その他の URL へのリクエストは、静的リソースへのアクセスとして Nuxt.js 開発サーバが処理する。

### Nuxt.js アプリの作成

Nuxt アプリジェネレータの[create-nuxt-app](https://github.com/nuxt/create-nuxt-app)をインストール済みにしておきます。

```bash
$ npm i -g create-nuxt-app
```

assets フォルダ以下に Nuxt.js アプリケーションのソースコードを配置したいので、アプリケーション名を assets にして Nuxt アプリケーションを作ります。利用するフレームワークを尋ねられますが、好きに選択してください。「Choose rendering mode」では「Single Page App」を選択、「Use axios module」では「yes」を選択してください。

```bash
$ create-nuxt-app assets
> Generating Nuxt.js project ...
? Project name [assets]
? Project description [My sublime Nuxt.js project]
? Use a custom server framework [none]
? Use a custom UI framework [none]
? Choose rendering mode [Single Page App]
? Use axios module [yes]
? Use eslint [yes]
? Use prettier [yes]
? Author name []
? Choose a package manager [npm]
```

「Choose rendering mode」で「Universal」を選択すれば、サーバーサイドレンダリングが出来ます。サーバ側で描画するのでサーバに負荷がかかりますし、サーバーサイドレンダリングができるようにするためにいくつか注意点があり、手っ取り早く開発するには向きません。「Single Page App」はブラウザ側のみで描画するモードのため、サーバーに優しく、アプリを作る上での制限も少ないため、こちらを選択します。

「Use axios module」は非同期の HTTP(S) 通信を簡単に記述できるようにするモジュールです。API サーバと通信するために必要です。

### Nuxt.js アプリの設定

localhost:3000/api へのアクセスを localhost:8080/api (SpringBoot) に転送するために Proxy モジュールを Nuxt.js アプリに追加します。「assets」フォルダに移動して、「@nuxtjs/proxy」モジュールを追加してください。

```bash
$ npm i --save @nuxtjs/proxy
```

assets/nuxt.config.js が Nuxt.js の設定ファイルです。Proxy モジュールを認識させるため、このファイルの「modules」の箇所に「'@nuxtjs/proxy'」を追加してください。

```assets/nuxt.config.js
   modules: [
     // Doc: https://github.com/nuxt-community/axios-module#usage
-    '@nuxtjs/axios'
+    '@nuxtjs/axios',
+    '@nuxtjs/proxy'
   ],
```

axios モジュールの設定を行います。「credentials」オプションは HTTP 通信のときクッキーなど秘匿情報を渡すかどうかを示すオプションです。本記事では扱いませんが、ログイン済みのときのみアクセスできる API を作るといった時に必要になります。「proxy」オプションは proxy モジュールと一緒に使うときに設定が必要です。

```assets/nuxt.config.js
   axios: {
     // See https://github.com/nuxt-community/axios-module#options
+    credentials: true,
+    proxy: true
   },
```

次は axios モジュールの設定を行います。

<blockquote>
localhost:3000/api へのアクセスを localhost:8080/api (SpringBoot) に転送する
</blockquote>

と言う話があります。ここでその設定を行います。また、Proxy 配下で Spring Boot を 動かすためには「X-Forwarded-Host」ヘッダでオリジナルのホスト名を知らせる必要があります。これがなぜ必要なのかは次で説明します。

```assets/nuxt.config.js
+
+  proxy: {
+    '/api/': {
+      target: 'http://localhost:8080',
+      headers: { 'X-Forwarded-Host': 'localhost:3000' }
+    }
   },
```

### X-Forwarded-Host ヘッダが必要な理由

Proxy 配下で Spring Boot を 動かす時に「X-Forwarded-Host」ヘッダでオリジナルのホスト名を知らせる必要がある理由を説明します。

HTTP 301,303 では Location ヘッダでリダイレクトを行いますが、Location ヘッダに指定できる URL は HTTP の仕様上、絶対パスとするのが正しいとされます。[RFC2616 日本語訳 14.30 Location](https://triple-underscore.github.io/-ja.html#section-14.30)に記載があります。Spring Boot(のベースとなる Spring Framework)でもこの仕様に準拠していると考えられ、Spring Boot でリダイレクト処理を行うと、Spring Boot が動くホスト名 http://localhost:8080/ から始まる URL にリダイレクトする動作になっています。

そこで、Spring Boot に「X-Forwarded-Host」ヘッダでオリジナルのホスト名を知らせることで、自分が動作しているホスト名ではなく、転送元のホスト名でリダイレクト URL を構築するようになり、http://localhost:3000/ から始まる URL にリダイレクトする動作になります。実際、URL を組み立てる org.springframework.web.util.UriComponentsBuilder クラスに「X-Forwarded-Host」ヘッダ指定がある場合、指定されたホスト名で URL を作る処理があります。[実際のソースコード](https://github.com/spring-projects/spring-framework/blob/v5.1.0.RELEASE/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java#L827)

### Spring Boot アプリの作成

Maven プロジェクトとして Spring Boot アプリを作っていきます。いつも通り「spring-boot-starter-parent」を親プロジェクトとします。ここでは「/api/time」でリクエスト時の日時を返すだけの簡素な API サーバを作ることにします。REST な WebAPI を作るために「Spring MVC」、ホットリロードできるようにするための「Developer Tools」を Maven の dependencies に追加します。JDK9で内部APIへのアクセスを制限する動作変更が行われています。JDK9以降のJDKで古いSpringBootバージョンを動かすとエラーが発生するため、SpringBootはなるべく新しいバージョンを使うようにしてください。

```server/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>com.example.server</artifactId>
  <version>1.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>

  <parent>
    <!-- Spring Core -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.4</version>
  </parent>

  <dependencies>
    <!-- Spring MVC -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Developer Tools -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
    </dependency>
  </dependencies>
</project>
```

Spring Boot で RESTful な WebAPI を作る方法は世の中でたくさん説明されていますので、以降は説明を省略です。

```server/src/main/java/com/example/Application.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

  public static void main(String[] arguments) {
    SpringApplication.run(Application.class, arguments);
  }
}
```

```server/src/main/java/com/example/controller/TimeController.java
package com.example.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RestController
public class TimeController {

  @RequestMapping("/api/time")
  public Map<String, String> index() {
    Map<String, String> response = new HashMap<>();
    response.put("date", new Date().toString());
    return response;
  }
}
```

## 開発の始め方

Nuxt.js 開発サーバを起動します。

```bash
$ npm run dev
```

別ターミナルで Spring Boot を起動します。

```bash
$ mvn spring-boot:run
```

どちらもホットリローディングが有効な状態で起動しています。ホットリローディングができることを確認するため、「/api/time」から取得した日時をトップページに表示するようにしてみます。次のように書き換えて保存すると、Nuxt.jsのホットリロード機能により即座にリロードされ反映されます。

```assets/pages/index.vue
<template>
  <h1>{{ date }}</h1>
</template>

<script>
module.exports = {
  data() {
    return {
      date: ''
    }
  },
  async asyncData({ app }) {
    const response = await app.$axios.get('/api/time')
    console.log(response.data.date)
    return { date: response.data.date }
  }
}
</script>

<style>
</style>
```

## デプロイ

SPA として Netlify へデプロイ、Spring Boot アプリとして Heroku へデプロイする方法を説明します。

```text
                                                 +-----------------+
               +------------+      /api          |                 |
               |            +------------------->|   Spring Boot   |
               |            |                    | (Heroku Server) |
               |  Netlify   |                    |                 |
-------------->| Web Server |                    +-----------------+
               |            |
               |            |
               +------------+
```

開発環境と同じ構成にします。


- Netlify の Web サーバの /api へのリクエストは Heroku サーバで動作する SpringBoot の /api に転送して、SpringBoot が処理する。
- その他の URL へのリクエストは、静的リソースへのアクセスとして Netlify の Web サーバが処理する。

### Netlify へのデプロイ

Netlify は静的なサイトをホスティングしてくれるサービスです。
単純にホスティングするだけでなく、git レポジトリと連携して自動ビルドをするといった機能もあります。また、netlify.toml という設定ファイルで自由にカスタマイズ出来ます。SSR こそ出来ないものSPAのデプロイ先としては十分な機能を持っています。

SPA/API サーバ構成の場合の netlify.toml ファイルによる設定方法を説明します。注意点は 2 点です。

- API サーバの設定では、転送対象のパスと転送先だけでなく、「X-Forwarded-Host」ヘッダでオリジナルサーバのホスト名も送るように設定します。
- 「/」以外の URL へ直接アクセスした場合はトップページと同じ内容を返すように設定します。この設定がないと「/」以外の URL に直接アクセスすると 404 NotFound です。

```assets/static/netlify.toml
# APIサーバの設定
[[redirects]]
  from = "/api/*"
  to = "https://<あなたのherokuホスト名>/api/:splat"
  status = 200
  [redirects.headers]
    X-Forwarded-Host = "<あなたのnetlifyホスト名>"

# SPAの設定
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
```

ここからは Netlify へのデプロイ手順です。Netlify コマンドラインツールをインストール済みにしておきます。

```bash
$ npm install -g netlify-cli
```

また、新しくターミナルを開いて、Netlify にログイン済みにしておきます。

```bash
$ netlify login
```

「assets」フォルダでビルドコマンドを実行すると、「assets/dist」に SPA アプリが生成されます。

```bash
$ npm run build
```

netlify コマンドを使って「dist」ディレクトリをデプロイします。初めてのデプロイの場合、色々尋ねられますが、お好きに答えてください。

```bash
$ netlify deploy
```

### Heroku へのデプロイ

Heroku は Java や Ruby、Node.js のサーバアプリをホスティングしてくれるサービスです。
Heroku へのデプロイ方法はいくつかあり、Heroku アプリの git レポジトリに push することでデプロイする方法が一般的ですが、今回は Maven を使ってデプロイします。このプロジェクトのようにトップディレクトリから一段下のディレクトリに Maven プロジェクトが配置される構成の場合、デプロイしにくいためです。

Maven から Heroku デプロイは「heroku-maven-plugin」を使い、

- jdkVersion タグの中にはお使いの JDK バージョン
- web タグの中には JavaVM オプション

を指定します。Heroku を使った経験がある方は Procfile ファイルを書いたことがあるかもしれませんが、「heroku-maven-plugin」を使う方法では作成不要です。

```server/pom.xml
   </dependencies>
+  <build>
+    <plugins>
+      <!-- Heroku deploy settings -->
+      <plugin>
+        <groupId>com.heroku.sdk</groupId>
+        <artifactId>heroku-maven-plugin</artifactId>
+        <version>2.0.6</version>
+        <configuration>
+          <jdkVersion>1.8</jdkVersion>
+          <processTypes>
+            <web>java -Duser.language=ja -Duser.country=JP -Duser.timezone=Asia/Tokyo -jar ./target/com.example.server-1.0.jar</web>
+          </processTypes>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 </project>
```

- 「-jar ./target/com.example.server-1.0.jar」

Maven でビルドすると、SpringBoot 自体とあなたが書いたアプリを同梱した jar ファイルが「target/com.example.server-1.0.jar」に生成されます。これを実行するという意味です。

- 「-Duser.language=ja -Duser.country=JP -Duser.timezone=Asia/Tokyo」

言語やタイムゾーンなど(いわゆるロケール)を日本向けにする設定です。

環境変数「HEROKU_API_KEY」を設定して、ビルド・デプロイします。環境変数「HEROKU_API_KEY」は https://dashboard.heroku.com/account ページに記載があります。

```bash
$ HEROKU_API_KEY=<APIキー> mvn clean install heroku:deploy -Dheroku.appName=<herokuアプリ名>
```

### おわりに

Nuxt.js、Spring Boot 共に解説サイトや書籍も多く、手軽に始めることができます。
運用は Netlify と Heroku にお任せることで、簡単に Web サービスを公開できる時代となりました。
ここで説明した内容はすべて無料の範囲で出来ますので、皆さんもぜひやってみましょう。