Heroku
spring-boot
Netlify
nuxt.js

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

Nuxt.js v2(SinglePageApplication、以下 SPA)と SpringBoot v2(API サーバ)な開発環境構築、Netlify/Heroku へのデプロイ方法をまとめました。主に Nuxt.js v2 と SpringBoot v2 の組み合わせに焦点を当てて説明します。

構築の前に

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

  • Node.js v10
  • JDK8
  • Maven v3

また、Netlify/Heroku のアカウントを作成しておいてください。

ディレクトリ構成

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

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

開発環境の作り方

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

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

localhost:3000 ドメインのみアクセスされるようにしておくと、クロスドメイン関連の対応が不要になります。この構成のポイントは 2 点です。

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

Nuxt.js アプリの作成

Nuxt アプリジェネレータのcreate-nuxt-appをインストール済みにしておきます。

$ npm i -g create-nuxt-app

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

$ 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 アプリの設定

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

$ 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 モジュールの設定を行います。

/api へのリクエストは localhost:8080 でアクセスできる SpringBoot に転送(Proxy)し、SpringBoot が処理する。

と言う話を覚えてますでしょうか。ここでその設定を行います。また、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に記載があります。Spring Boot(のベースとなる Spring Framework)でもこの仕様に沿っていると考えられ、Spring Boot でリダイレクト処理を行うと、Spring Boot が動くホスト名 http://localhost:8080/ から始まる URL にリダイレクトする動作になっています。

そこで、Spring Boot に「X-Forwarded-Host」ヘッダでオリジナルのホスト名を知らせることで、自分が動作しているホスト名ではなく、オリジナルのホスト名でリダイレクト URL を構築するように指示します。実際、URL を組み立てる UriComponentsBuilder クラスに「X-Forwarded-Host」ヘッダ指定がある場合、指定されたホスト名で URL を作る処理があります。実際のソースコード

Spring Boot アプリの作成

Maven プロジェクトとして Spring Boot アプリを作っていきます。いつも通り「spring-boot-starter-parent」を親プロジェクトとします。ここでは「/api/time」でリクエスト時の日時を返すだけの簡素な API サーバを作ることにします。REST な WebAPI を作るために「Spring MVC」、ホットリロードできるようにするための「Developer Tools」を Maven の dependencies に追加します。

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.0.5.RELEASE</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 開発サーバを起動します。

$ npm run dev

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

$ mvn spring-boot:run

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

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

<script>
export default {
  async asyncData({ app }) {
    const response = await app.$axios.get('/api/time')
    return { date: response.data.date }
  }
}
</script>

<style>
</style>

デプロイ

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

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

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

  • /api へのリクエストは Heroku サーバで動作する SpringBoot に転送して、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 コマンドラインツールをインストール済みにしておきます。

$ npm install -g netlify-cli

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

$ netlify login

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

$ npm run build

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

$ 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 ページに記載があります。

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

おわりに

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