KubernetesとNode.jsでマイクロサービスを作成する 4/6 Webサービス

# 第4章 Webサービス
本章ではNode.jsを利用して、TwitterライクなサービスのUIを作成していきます。

分量が多いので、実装の細かい部分の説明は省略しますが、もし気になる方がいれば、BFF部分と認証部分を以下の記事で詳細に説明しているので、ご参照ください。

[Nuxt.jsとPassport.jsでマイクロサービス用の認証機能をもったフロントエンド + BFFを構築する](https://qiita.com/reireias/items/2bc1b33dfb0e63f31517)

## チュートリアル全体
- [第1章 概要](https://qiita.com/reireias/items/6a78d6b8df781a76f94f)
- [第2章 Tweetサービス](https://qiita.com/reireias/items/82cfbb285130caf3e091)
- [第3章 Userサービス](https://qiita.com/reireias/items/27e21896b701360bc152)
- [第4章 Webサービス](https://qiita.com/reireias/items/204c080cd5ec1bf432fd)
- [第5章 Dockerを使ったサービス構築](https://qiita.com/reireias/items/efefe718d74ed8447931)
- [第6章 Kubernetes with minikube](https://qiita.com/reireias/items/084888abb3a757c0c5e2)

## 構成
![microservice-tutorial01.png](https://qiita-image-store.s3.amazonaws.com/0/214526/36201231-6030-9ac3-03ad-aaa99619eed2.png)

## アーキテクチャ
Webサービスでは下記のフレームワーク/モジュールを利用します。
リンクをつけておくので、気になる方はチェックしてみてください。

- [Vue.js](https://vuejs.org/index.html) モダンなjs向けUIライブラリ
- [Vuetify.js](https://vuetifyjs.com/en/) Vue.js用マテリアルデザインライブラリ
- [Nuxt.js](https://nuxtjs.org/) SPA/SSRも簡単にできるVue.jsフレームワーク
- [Express.js](https://expressjs.com/) Node.js向けサーバー
- [Passport.js](http://www.passportjs.org/) GitHubやGoogleなどとのOAuth連携を簡単に導入できる

## サイト構成
### 画面
まずは画面構成のイメージ図を確認しましょう。

1. トップページ(path: `/`)
   未ログイン状態で表示されるトップページです。
   特に表示するロゴとかイメージはないので、採用技術などを並べています。
   ![web_05.png](https://qiita-image-store.s3.amazonaws.com/0/214526/64175003-f977-5d99-a40c-777e56124fdf.png)

2. ホーム画面(path: `/home`)
   いわゆる**タイムライン**です。
   右下のボタンを押すことで、下記のTweetダイアログを開きます。
   このページは未認証状態でアクセスすると`/`へリダイレクトするように設定します。
   ![web_06.png](https://qiita-image-store.s3.amazonaws.com/0/214526/695858c0-2b0b-80d5-bda2-60e94d8bdfb3.png)

3. Tweetダイアログ(path: `/home`)
   Tweetを投稿するためのダイアログです。
   右下のボタンを押すか、`Ctrl + Enter`で投稿できるようにします。
   ![web_07.png](https://qiita-image-store.s3.amazonaws.com/0/214526/9f7362bd-8e17-f479-582f-12d28fb3d7bf.png)

4. ユーザー一覧(path: `/users`)
   本家のように検索を実装するのは面倒なので、全ユーザー一覧を表示します。
   フォロー/アンフォローをボタンにより実施できます。
   このページは未認証状態でアクセスすると`/`へリダイレクトするように設定します。
   ![web_08.png](https://qiita-image-store.s3.amazonaws.com/0/214526/18bfef12-33c0-480a-9fcd-91faae453428.png)

5. 認証用コールバックページ(`/callback`)
   GitHubとのOAuth認証を行うために、コールバック用のページも必要となります。
   このページはシンプルにローディングのアニメを出すだけとします。

## 実装
それでは実装していきましょう。

Node.jsによるUI実装に興味が無い場合は、下記のファイルはすべて[このリポジトリ]()中にあるので、forkすればOKです。

### リポジトリ作成
GitHubにて、`microservice-sample-web`という名前で作成しましょう。

### プロジェクトの初期化
`create-nuxt-app`を利用してプロジェクトを初期化します。

```sh
yarn create nuxt-app
# 対話に下記のように答えていく
? Project name web
? Project description Web service.
? Use a custom server framework express
? Choose features to install Linter / Formatter, Prettier, Axios
? Use a custom UI framework vuetify
? Use a custom test framework ava
? Choose rendering mode Universal
? Author name reireias
? Choose a package manager yarn
```

まずはプロジェクトが作成されているか、サーバーを起動して確認してみましょう。

```sh
yarn dev
```

起動後、`http://localhost:3000`へアクセスすると、下記のような画面が表示されればOKです。

![web_01.png](https://qiita-image-store.s3.amazonaws.com/0/214526/db2cf622-4325-04f0-f91e-54e30ec70563.png)

`yarn lint`で`nuxt.config.js`に`eslint`の警告が出てしまうので抑制しておきます。
`express`側に`nuxt.config.js`を読みこませる都合で`import`構文は利用できないためです。

```js:nuxt.config.js
/* eslint nuxt/no-cjs-in-config: 0 */
const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
const pkg = require('./package')

module.exports = {
  mode: 'universal',

  /*
  ** Headers of the page
  */
  head: {
    title: pkg.name,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: pkg.description }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },

  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },

  /*
  ** Global CSS
  */
  css: ['~/assets/style/app.styl'],

  /*
  ** Plugins to load before mounting the App
  */
  plugins: ['@/plugins/vuetify'],

  /*
  ** Nuxt.js modules
  */
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios'
  ],
  /*
  ** Axios module configuration
  */
  axios: {
    // See https://github.com/nuxt-community/axios-module#options
  },

  /*
  ** Build configuration
  */
  build: {
    transpile: ['vuetify/lib'],
    plugins: [new VuetifyLoaderPlugin()],
    loaders: {
      stylus: {
        import: ['~assets/style/variables.styl']
      }
    },

    /*
    ** You can extend webpack config here
    */
    extend(config, ctx) {
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }
}
```

また、`husky`による`pre-commit`時の自動lintも設定しておきましょう。

```sh
yarn add -D husky
```

```json:package.json
...
  "husky": {
    "hooks": {
      "pre-commit": "yarn lint"
    }
  },
...
```

`stylus`と`stylus-loader`というパッケージが`devDependencies`にありますが、これらは後述のproductionビルドで必要になるため、次のコマンドで`dependencies`側に移しておきます。

```sh
yarn remove stylus stylus-loader
yarn add stylus stylus-loader
```

以上でプロジェクトの初期設定は完了です。

### indexページの作成

続いてトップページ(パスは`/`)から作成していきます。

まずは不要なページやコンポーネントを削除しましょう。

```sh
rm pages/*.vue
rm components/*.vue
```

全ページに共通するレイアウトを作成します。

```vue:layouts/default.vue
<template>
  <nuxt />
</template>
```

`pages/index.vue`を実装していきます。
トップページに記載したい内容は特に無いので、今回は採用技術を列挙しています。

```vue:pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <div class="text-xs-center">
        <h1>Microservice Sample App</h1>
      </div>
      <br>
      <h2>Technologies</h2>
      <ul>
        <li>Kubernetes</li>
        <li>Docker</li>
        <li>Nuxt.js</li>
        <li>Vue.js</li>
        <li>OAuth</li>
        <li>Express.js</li>
        <li>MongoDB</li>
      </ul>
    </v-flex>
  </v-layout>
</template>
```

以上でトップページの実装は完了です。
`yarn dev`でサーバーを起動し、アクセスしてみましょう。

![web_02.png](https://qiita-image-store.s3.amazonaws.com/0/214526/6673de36-6fbd-e212-7c81-62651b2b68f9.png)

### layoutとstoreの作成
`vuex`の`store`を作成します。
この`store`を利用して、ログイン状態を保持します。

```js:store/index.js
export const state = () => ({
  user: null,
  auth: false
})

export const mutations = {
  login(state, payload) {
    state.auth = true
    state.user = payload
  },
  logout(state) {
    state.auth = false
    state.user = null
  }
}
```

レイアウトも実装してしまいましょう。

全ページ共通として、HomeタブとUsersタブ、右上のログインボタンを表示します。
ログインボタン下の画像のように、はログイン時にはアバターアイコン兼ログアウトボタンとして表示されるものとします。

ログイン前
![web_08.png](https://qiita-image-store.s3.amazonaws.com/0/214526/eff935f2-a693-4b11-a1d9-c1b68eec617f.png)

ログイン後
![web_09.png](https://qiita-image-store.s3.amazonaws.com/0/214526/143ef521-2286-22ee-e392-0d92976fbc6e.png)


ログインボタンはコンポーネントとして切り出して実装します。

```vue:layouts/default.vue
<template>
  <v-app>
    <v-tabs
      icons-and-text
      dark
      color="primary"
      slider-color="accent"
    >
      <v-tab to="/home">
        Home
        <v-icon>home</v-icon>
      </v-tab>
      <v-tab to="/users">
        Users
        <v-icon>people</v-icon>
      </v-tab>
      <v-spacer />
      <avatar />
    </v-tabs>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import Avatar from '~/components/Avatar.vue'
export default {
  components: {
    Avatar
  },
  data() {
    return {
      title: 'Microservice Sample'
    }
  }
}
</script>
```

ログインボタン/アバターアイコンのコンポーネントを下記のように実装します。

```vue:components/Avatar.vue
<template>
  <div class="avatar">
    <template v-if="$store.state.auth">
      <v-menu bottom left>
        <v-btn slot="activator" icon>
          <v-avatar size="32px">
            <img :src="$store.state.user.avatarUrl">
          </v-avatar>
        </v-btn>
        <v-list>
          <v-list-tile href="/api/auth/logout">
            <v-list-tile-title>Logout</v-list-tile-title>
          </v-list-tile>
        </v-list>
      </v-menu>
    </template>
    <template v-else>
      <v-btn
        round
        outline
        href="/api/auth/github"
      >
        Login
      </v-btn>
    </template>
  </div>
</template>


<style>
.v-tabs .v-btn {
  text-transform: none;
}
.avatar {
  align-self: center;
}
.v-menu__activator {
  height: 100%;
}
</style>
```

ここまでできたら`yarn dev`でサーバーを起動してアクセスしてみましょう。

トップページにタブやログインボタンが表示されるはずです。

ただ、ログイン機能を実装していないので、実際にログインすることはまだできません。

![web_03.png](https://qiita-image-store.s3.amazonaws.com/0/214526/6d3cd92b-dab7-357a-9a0a-1f4ae83b7a61.png)

### Passport.jsによるGitHub認証の追加
続いてログイン機能を実装します。

必要なモジュールを追加します。

```sh
yarn add passport passport-github2 express-session morgan body-parser
```

`Passport.js`によるログイン処理を行うサーバーサイドを実装します。
このあたりは`Passport.js`のサンプルを参考に実装しています。

```js:server/index.js
const express = require('express')
const morgan = require('morgan')
const session = require('express-session')
const passport = require('passport')
const { Strategy } = require('passport-github2')
const bodyParser = require('body-parser')
const { Nuxt, Builder } = require('nuxt')
const app = express()

app.use(morgan('short'))
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(
  session({
    secret: process.env.SESSION_SECRET || 'secret',
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: 'auto'
    }
  })
)
app.use(passport.initialize())
app.use(passport.session())

passport.use(
  new Strategy(
    {
      clientID: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      callbackURL: process.env.CALLBACK_URL || 'http://localhost:3000/callback'
    },
    (accessToken, refreshToken, profile, done) => {
      process.nextTick(() => {
        const name = profile.username
        const avatarUrl = profile.photos[0].value
        // TODO: create or update user data
        return done(null, {
          _id: 'dummy',
          name: name,
          avatarUrl: avatarUrl
        })
      })
    }
  )
)

passport.serializeUser((user, done) => {
  done(null, user)
})
passport.deserializeUser((obj, done) => {
  done(null, obj)
})

app.get(
  '/api/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
)
app.get(
  '/api/auth/github/callback',
  passport.authenticate('github'),
  (req, res) => {
    res.json({ user: req.user })
  }
)
app.get('/api/auth/logout', (req, res) => {
  req.logout()
  res.redirect('/')
})
app.get('/api/session', (req, res) => {
  res.json({ user: req.user })
})

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

async function start() {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const {
    host = process.env.HOST || '127.0.0.1',
    port = process.env.PORT || 3000
  } = nuxt.options.server

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  app.listen(port, host)
}
start()
```

GitHubからリダイレクトするcallbackページを実装します。

```vue:pages/callback.vue
<template>
  <center>
    <v-progress-circular
      :size="70"
      :width="7"
      color="primary"
      indeterminate
    />
  </center>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  async mounted() {
    const res = await this.$axios.get('/api/auth/github/callback', {
      params: this.$route.query
    })
    const user = {
      _id: res.data.user._id,
      name: res.data.user.name,
      avatarUrl: res.data.user.avatarUrl
    }
    this.$store.commit('login', user)
    this.$router.push('/home')
  }
}
</script>
```

[こちら](https://qiita.com/reireias/items/2bc1b33dfb0e63f31517#github%E8%AA%8D%E8%A8%BC%E3%81%AE%E6%BA%96%E5%82%99)を参考に、GitHubでOAuth用のClientIDとClientSecretを取得してください。
その際、callbackURLは`http://localhost:3000/callback`とします。

上記で発行したClientIDとClientSecretを指定しながらサーバーを起動します。

```sh
GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy yarn dev
```

トップページへアクセスし、ログインボタンを押してみましょう。

GitHub側のOAuthの認証画面へ遷移し、実際にログインができるはずです。
しかし、ログイン成功後に遷移する`/home`が未実装のため、ログイン後に画面にはエラーが表示されます。

![web_04.png](https://qiita-image-store.s3.amazonaws.com/0/214526/7921cd31-b3cc-8573-ffaa-9aa3d6256be5.png)

### ログイン状態の場合のみアクセス可能なページの実現
`/home`と`/users`を実装していきましょう。

まずは、`/home`で表示するタイムラインをコンポーネントとして実装します。

```vue:components/Timeline.vue
<template>
  <v-container fluid grid-list-xs>
    <v-layout justify-center row wrap>
      <v-flex
        v-for="tweet in timeline"
        :key="tweet._id"
        class="tweet"
        xs12
      >
        <tweet :tweet="tweet" />
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import Tweet from '~/components/Tweet.vue'
export default {
  components: {
    Tweet
  },
  data() {
    return {
      timeline: []
    }
  },
  async mounted() {
    this.timeline = await this.$axios.$get('/api/timeline')
  },
  methods: {
    async update() {
      this.timeline = await this.$axios.$get('/api/timeline')
    }
  }
}
</script>

<style>
.tweet {
  margin: 4px;
}
</style>
```

タイムライン中の1Tweetもコンポーネントとして実装します。

```vue:components/Tweet.vue
<template>
  <v-card>
    <v-layout row wrap>
      <v-btn icon>
        <v-avatar size="32px">
          <img :src="tweet.avatarUrl">
        </v-avatar>
      </v-btn>
      <div>
        <span class="name">
          {{ tweet.name }}
        </span>
        <span class="created-at">
          ・ {{ formatDate(tweet.createdAt) }}
        </span>
        <span class="content">
          {{ tweet.content }}
        </span>
      </div>
    </v-layout>
  </v-card>
</template>

<script>
export default {
  props: {
    tweet: {
      type: Object,
      required: true
    }
  },
  methods: {
    formatDate(date) {
      return new Date(date).toLocaleString('ja-JP')
    }
  }
}
</script>

<style>
.name {
  font-size: 16px;
  font-weight: bold;
}
.created-at {
  color: gray;
}
.content {
  margin: 0px;
  font-size: 16px;
  white-space: pre-line;
}
</style>
```

画面右下に表示するのTweetボタンと、開かれるダイアログをコンポーネントとして実装します。

```vue:components/TweetDialog.vue
<template>
  <v-fab-transition>
    <v-dialog v-model="dialog" max-width="600px">
      <v-btn
        slot="activator"
        fixed
        bottom
        right
        fab
        dark
        color="primary"
      >
        <v-icon>edit</v-icon>
      </v-btn>
      <v-card>
        <v-card-title>
          <span class="headline">
            Tweet
          </span>
        </v-card-title>
        <v-card-text>
          <v-textarea
            v-model="content"
            label="What's happening?"
            box
            autofocus
            @keyup.enter="tweetEnter"
          />
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn
            icon
            flat
            color="red"
            @click="dialog = false"
          >
            <v-icon>close</v-icon>
          </v-btn>
          <v-btn
            icon
            flat
            color="green"
            @click="tweet"
          >
            <v-icon>check</v-icon>
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-fab-transition>
</template>

<script>
export default {
  data() {
    return {
      dialog: false,
      content: null
    }
  },
  methods: {
    async tweet() {
      this.dialog = false
      await this.$axios.post('/api/tweets', { content: this.content })
      this.content = null
      this.$emit('tweeted')
    },
    async tweetEnter(event) {
      if (event.ctrlKey) {
        await this.tweet()
      }
    }
  }
}
</script>

<style>
.v-card__title {
  padding: 8px;
}
.v-card__text {
  padding: 8px;
}
.v-card__actions {
  padding: 8px;
}
</style>
```

これらを表示する`pages/home.vue`を実装しましょう。

```vue:pages/home.vue
<template>
  <v-flex xs10 offset-xs1>
    <timeline ref="timeline" />
    <tweet-dialog @tweeted="tweeted" />
  </v-flex>
</template>

<script>
import Timeline from '~/components/Timeline.vue'
import TweetDialog from '~/components/TweetDialog.vue'
export default {
  components: {
    Timeline,
    TweetDialog
  },
  methods: {
    tweeted() {
      this.$refs.timeline.update()
    }
  }
}
</script>
```

続いて`/users`の実装をします。

まず、1ユーザーの表示をコンポーネント化します。

```vue:components/User.vue
<template>
  <v-card>
    <v-btn icon>
      <v-avatar size="32px">
        <img :src="user.avatarUrl">
      </v-avatar>
    </v-btn>
    <span class="name">
      {{ user.name }}
    </span>
    <v-card-actions>
      <v-spacer />
      <v-btn
        v-if="user.follow"
        color="red"
        outline
        @click="unfollow(user)"
      >
        unfollow
      </v-btn>
      <v-btn
        v-else
        color="primary"
        outline
        @click="follow(user)"
      >
        follow
      </v-btn>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    }
  },
  methods: {
    async follow(user) {
      const res = await this.$axios.post('/api/follow', { _id: user._id })
      if (res.status === 200) {
        user.follow = true
      }
    },
    async unfollow(user) {
      const res = await this.$axios.delete('/api/follow', {
        params: { _id: user._id }
      })
      if (res.status === 200) {
        user.follow = false
      }
    }
  }
}
</script>

<style>
.name {
  font-size: 16px;
  font-weight: bold;
}
</style>
```

上記を利用して、ユーザー一覧を表示するページを実装します。

```vue:pages/users.vue
<template>
  <v-flex xs10 offset-xs1>
    <v-container fluid grid-list-xs>
      <v-layout justify-start row wrap>
        <v-flex
          v-for="user in users"
          :key="user._id"
          class="users"
          xs6
        >
          <user :user="user" />
        </v-flex>
      </v-layout>
    </v-container>
  </v-flex>
</template>

<script>
import User from '~/components/User.vue'
export default {
  components: {
    User
  },
  data() {
    return {
      users: []
    }
  },
  async asyncData({ app }) {
    const res = await app.$axios.get('/api/users')
    return {
      users: res.data
    }
  }
}
</script>
```

ここまでできたら、ローカルでこれまでに実装した3つのサービスを起動して動作を確認してみましょう。
それぞれ別ターミナルを使って起動します。
また、MongoDBは`localhost:27017`で起動している必要があります。

```sh
# (ターミナル1で)Userサービスをポート3001で起動
PORT=3001 yarn dev
# (ターミナル2で)Tweetサービスをポート3002で起動
PORT=3002 yarn dev
# (ターミナル3で)UserサービスとTweetサービスのエンドポイントと、GitHubのOAuth認証情報を指定しながらWebサービスを起動
GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy USER_SERVICE=http://localhost:3001 TWEET_SERVICE=http://localhost:3002 yarn dev
```

以下を確認してみましょう。

- ユーザー一覧からフォロー/アンフォローできる
- タイムラインにフォローしたユーザーのツイートが表示される
- ツイートができる

### リロード時のsession取得
現状の実装では`F5`キー等で画面をリロードすると、ログイン状態が解除されてしまいます。
これはブラウザ側で持っている`vuex`の`store`中の情報がリロードでクリアされてしまうためです。
そこで、`nuxtServerInit`という機能を利用してリロードを含むページへの最初のアクセス時に、サーバーサイドのsession情報を取得して`store`へロードする実装を行います。

`nuxtServerInit`に関する詳しい情報は[こちら](https://nuxtjs.org/guide/vuex-store#the-nuxtserverinit-action)を参照してください。

```js:store/index.js
export const state = () => ({
  user: null,
  auth: false
})

export const mutations = {
  login(state, payload) {
    state.auth = true
    state.user = payload
  },
  logout(state) {
    state.auth = false
    state.user = null
  }
}

export const actions = {
  nuxtServerInit({ dispatch }, { req }) {
    return dispatch('getSession')
  },
  async getSession({ commit }) {
    const session = await this.$axios.$get('/api/session')
    if (session && session.user) {
      commit('login', session.user)
    }
  }
}
```

サーバーを起動し、ログイン後、F5を押してリページをリロードしてみましょう。
先程までとは異なり、ログイン状態が保持されるはずです。

### 未認証時のリダイレクト
現状、未認証状態でも画面上部のタブを押すことにより、`/home`や`/users`ページへアクセスできてしまいます。(ユーザーIDが取得できないので、APIのレスポンスがエラーになりますが)

これを回避するために、特定のページ以外は未認証状態でアクセスすると、トップページへリダイレクトするように実装しましょう。

この設定にはNuxt.jsの`router`の設定を利用します。

`middleware/session.js`にリダイレクトの実装を行います。

```js:middleware/session.js
export default ({ store, route, redirect }) => {
  if (!store.state.auth && !['/', '/callback'].includes(route.path)) {
    return redirect('/')
  }
}
```

上記を設定ファイル中で`router`に設定します。

```js:nuxt.config.js
...
  router: {
    middleware: 'session'
  },
  /*
  ** Axios module configuration
  */
  axios: {
    proxy: true
  },
...
```

これだけでリダイレクトの設定は完了です。

サーバーを起動し、未認証状態で`/home`や`/users`へアクセスしてみましょう。
リダイレクトされるようになったはずです。

## まとめ
第4章では作成済みのTweetサービスとUserサービスをバックエンドとして利用するWebサービスを構築しました。

ローカルでそれぞれのサービスを起動し、動作することを確認しました。

次の章では、Dockerイメージを作成し、Docker上でこれらのサービスを動かしてみます。

次章: [第5章 Dockerを使ったサービス構築](https://qiita.com/reireias/items/efefe718d74ed8447931)