のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

App Engineを試す vol4 ~サービス同士を連携させる~

概要

AppEngine勉強日記、4日目。

この記事では、defaultサービス上のNuxtからexpressを経てapisサービスのAPIにアクセスするところを検証します。

すごい、4日連続で続いたよ。アドベントカレンダーいけんじゃね?

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

作るサービスの仕様

  1. 題名と本文を投稿できる
  2. 投稿すると、題名と本文を閲覧できるページに遷移する

以上。いわゆるnopasteと呼ばれるサービスです。slackの時代、もう知らないひともいるんじゃなかろうか。

あ、コードやテストについては、日付更新まで2hしかなかったのでとりあえずなるはやの全力で書いたので結構雑です。 そのうちリファクタする。

defaultサービスの変更

通信用にaxiosをとりあえず入れる。

> cd root
> npm i axios

ページの作成

ついでにlayoutも作るけど省略。

./root/pages/index.vue

投稿用の画面を作る。 titlebodyを入力して、postを押下すると/apis/postsPOSTリクエストを投げる。

<template>
  <form class="form">
    <div class="form-group">
      <label for="title" class="form-label">Title</label>
      <div>
        <input type="text" id="title" class="form-control" v-model="input.title">
      </div>
    </div>
    <div class="form-group">
      <label for="body" class="form-label">Body</label>
      <div>
        <textarea id="body" rows="10" v-model="input.body"></textarea>
      </div>
    </div>
    <div class="form-group">
      <button type="button" class="btn-primary" :disabled="!is_valid" @click="submit">Post</button>
    </div>
  </form>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      input: { title: null, body: null }
    }
  },
  methods: {
    is_valid() {
      return this.body
    },
    async submit() {
      const { data } = await axios.post('/apis/posts', {
        title: this.input.title,
        body: this.input.body
      })
      this.$router.push(`/posts/${data.id}`)
    }
  }
}
</script>

pages/posts/_id.vue

閲覧画面の作成。 /apis/posts/:idから文書情報を取得して表示する。

<template>
  <div v-if="content">
    <h2>{{content.title || 'no title'}}</h2>
    <div>
      {{content.body}}
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return { content: null }
  },
  mounted() {
    axios.get(`/apis/posts/${this.$route.params.id}`)
      .then(({ data }) => {
        this.content = data
      })
  }
}
</script>

expressサーバにAPIを追加

POSTリクエストを受け取れるようにbodyParserをセット。

GET /apis/posts/:idPOST /apis/posts のルーティングを作成。 apisサービスのAPIを立たけるようにする。

このとき、apisサービスのオリジンは環境変数から渡せるようにする。

diff --git a/root/app.js b/root/app.js
index 35a950f..c9120af 100644
--- a/root/app.js
+++ b/root/app.js
@@ -5,17 +5,34 @@ const express = require('express');
 const app = express();
 const { Nuxt, Builder } = require('nuxt')

+app.use(express.json())
+app.use(express.urlencoded({ extended: true }));
+
 // Nuxt.js をオプションとともにインスタンス化する
 const config = require('./nuxt.config')
 config.dev = process.env.NODE_ENV !== 'production'
 const nuxt = new Nuxt(config)

-app.get('/message', (req, res) => {
-  res
-    .status(200)
-    .send('Hello, world!')
-    .end();
-});
+const axios = require('axios')
+app.post('/apis/posts', (req, res) => {
+  const data = req.body
+  axios.post(`${process.env.APIS_ORIGIN}/posts`, {
+    title: data.title,
+    body: data.body
+  })
+    .then(({ data }) => {
+      res.json({ id: data.id })
+    })
+})
+app.get('/apis/posts/:id', (req, res) => {
+  axios.get(`${process.env.APIS_ORIGIN}/posts/${req.params.id}`)
+    .then(({ data }) => {
+      res.json(data)
+    })
+    .catch(({ response }) => {
+      res.status(response.status).json(response.body)
+    })
+})

 app.use(nuxt.render)

app.yamlにapisサービスのオリジンを設定

diff --git a/root/app.yaml b/root/app.yaml
index eed1e47..6c4cf07 100644
--- a/root/app.yaml
+++ b/root/app.yaml
@@ -1 +1,4 @@
 runtime: nodejs10
+
+env_variables:
+  APIS_ORIGIN: https://apis-dot-astral-web-260712.appspot.com

deploy

> npm run build
> gcloud app deploy

これだけではまだ見れないので、続けてapisサービスを作っていく

apisサービスの変更

GET /posts/:idにリクエストがきたらサービスから任意のIDのレコードを取得して返却。

POST /postsにリクエストが来たらサービスにtitlebodyを渡してidを返却。

import { Controller, Post, Body, Get, Param, NotFoundException } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postService: PostsService) {
  }

  @Get(':id')
  async fetchById(@Param('id') id: string): Promise<{id: number, title: null|string, body: string}> {
    const row = await this.postService.fetchById(Number(id));
    if (!row) {
      throw new NotFoundException();
    }
    return row;
  }

  @Post('')
  async save(
    @Body('title') title: null|string,
    @Body('body') body: string,
  ): Promise<{ id: number }> {
    return await this.postService.save({ title, body });
  }
}

サービスを定義する。面倒なのでメモリ上にレコードを保持する。

import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {
  private rows: Array<{ id: number, title: string|null, body: string }>;

  constructor() {
    this.rows = [];
  }

  async fetchById(id: number) {
    return this.rows.find((row) => row.id === id);
  }

  async save({ title, body }: { title: string|null, body: string }): Promise<{ id: number }> {
    const id = this.rows.length + 1;
    this.rows.push({ id, title, body });
    return { id };
  }
}

PostsModuleを定義して、ControllerとServiceを関連付ける。

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

最後にAppModuleにPostsModuleを追加して完了

diff --git a/apis/src/app.module.ts b/apis/src/app.module.ts
index 8662803..5c11c51 100644
--- a/apis/src/app.module.ts
+++ b/apis/src/app.module.ts
@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { AppController } from './app.controller';
 import { AppService } from './app.service';
+import { PostsModule } from './posts/posts.module';

 @Module({
-  imports: [],
+  imports: [PostsModule],
   controllers: [AppController],
   providers: [AppService],
 })

deployする

> npm run build
> gcloud app deploy

動作を確認する

gcloud app browse -s defaultすると投稿画面が開く。 投稿フォーム実行画面

値を入力してPostすると表示ページに遷移する。 表示画面

できたー!

まとめ

なんとか4日目の対応を乗り切ったぜ...!

ここまで勧めてみたけど、いくつか気になる点がある。

  • メモリ上に乗せてるけどインスタンス増えたら任意のデータが取れないよね...?
  • apisサービスのURIに直接アクセスできちゃうよね...?

今後の課題にしていきます!

次回予告

というわけで、明日はデータストア周りさわってみたいなー!