AWS Amplify & Nuxt 使って爆速でWebアプリ作る

AWS でWebアプリ作る機会があったので、色々調べるとAmplifyが便利そうだったので使ってみました。

今回作成するアプリの主な機能は以下：

- NuxtベースのTODOアプリ
- Lambda使ったGraphQLのAPI
- DynamoDBへのデータ保存/読込
- DynamoDBのデータとリアルタイム同期
- Cognitoによる認証機能

基本的には[公式チュートリアル](https://docs.amplify.aws/start/q/integration/vue)をなぞってますが、Nuxtで使うに当たりいくつか変更点があるので、Nuxt使いに役立てば幸いです。

## Amplifyの初期設定

まずAmplifyのCLIをインストールしましょう。

```bash
$ npm install -g @aws-amplify/cli
```

インストールしたら`amplify`コマンドが使えるようになってるはずです。早速`configure`で設定をしてみます。

```bash
$ amplify configure
Scanning for plugins...
Plugin scan successful
Follow these steps to set up access to your AWS account:

Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue
```

上記のようなログが流れて、AWSへのサインインが求められるので、サインインしてください。その次にまたコンソール戻りましょう。

戻ったら、regionの設定です。東京なら`ap-northeast-X`を選べば良いはず。user nameはデフォルトでもいいですが、わかりやすく`amplify-admin`としてみました。

```bash
Specify the AWS Region
? region:  ap-northeast-1
Specify the username of the new IAM user:
? user name:  amplify-admin
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=amplify-admin&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess
Press Enter to continue
```

ここでAWS Consoleがブラウザで立ち上がり、ユーザー追加が求められます。基本的に全部デフォルトでOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/3e5ee8ee-0774-b77a-dd57-a348939ac343.png)

このままで次の画面進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/b71b8f73-359e-8215-9722-4120fe38c591.png)

これもこのままで次に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/f15d29a9-53d4-4823-5077-93e043e87b3d.png)

最終的に以下の画面が表示されます。このページはまだ閉じないでください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/6796ff7e-09e1-3d85-23ba-24604ddcf2c2.png)

コンソール画面に戻ります。

戻ったら、成功画面に表示されてる値を元にキーを入力してってください。キーは流出しないように注意して管理すること。Profile NameはなんでもOKです。今回は`default`にしました。

```bash
Enter the access key of the newly created user:
? accessKeyId:  # 成功画面に表示されてるアクセスキーID
? secretAccessKey:  # 成功画面に表示されてるシークレットアクセスキー
This would update/create the AWS Profile in your local machine
? Profile Name:  default

Successfully set up the new user.
```

## Nuxtアプリを用意する

Nuxtを`create-nuxt-app`使ってベースとなるアプリを用意します。アプリを作りたいディレクトリで以下のコマンド実行します。各オプションはお好みで。

```bash
$ npx create-nuxt-app nuxt-amplify

create-nuxt-app v3.2.0
✨  Generating Nuxt.js project in nuxt-amplify
? Project name: nuxt-amplify
? Programming language: JavaScript 
? Package manager: Npm
? UI framework: Vuetify.js
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier    
? Testing framework: Jest
? Rendering mode: Single Page App    
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
```

完了したら起動して動作するか試してみましょう。

```bash
$ npm run dev

(省略)

i Waiting for file changes                                                                                                                              
i Memory usage: 415 MB (RSS: 516 MB)                                                                                                                            
i Listening on: http://localhost:3000/      
```

http://localhost:3000/ にアクセスして下の画面が見えたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/7445427b-1b32-05c5-827c-c0f0d8ff2245.png)

ついでに、次以降の作業のために`generate`コマンドを使ってファイルを生成しておきます。

```bash
$ npm run generate
```

distディレクトリが作られて中身が入ってればOKです。

## Amplifyのバックエンドを作成する

Nuxtプロジェクトのルートディレクトリで以下のコマンドを打ち、設定を進めます。

```bash
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project nuxtamplify
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  .
? Distribution Directory Path: dist
? Build Command:  npm.cmd run-script generate
? Start Command: npm.cmd run-script start
```

そうすると、以下の内容が更新されます。

- `/amplify`ディレクトリが作成される。このディレクトリにはバックエンドの定義をコードベースで実装していき、Infrastructure as Codeを実現できる。
- `aws-exports.js`が作成されてバックエンドの情報が保存される。
- `.gitignore`が更新されていくつかのファイルが指定される。
- AWS Console上にプロジェクトが作られる。　???

## Amplifyのフロント用ライブラリをインストールする

続いて、Nuxt側から使うフロント用のAmplifyライブラリをnpmからインストールします。

```bash
$ npm install aws-amplify @aws-amplify/ui-vue
```

そしてNuxt側で`/plugins`ディレクトリに`amplify.js`ファイルを作成し、プラグインとしてインストールしたライブラリを読み込みます。

```js:amplify.js
import Vue from 'vue'
import Amplify from 'aws-amplify'
import '@aws-amplify/ui-vue'
import awsExports from '../aws-exports'

Amplify.configure(awsExports)
Vue.use(Amplify)
```

作成したプラグインを読み込むよう`nuxt.config.js`の`plugins`配列で指定します。

```js:nuxt.config.js
// ...
  plugins: [{ src: '~/plugins/amplify.js', ssr: false }],
// ...
```

この状態で`npm run dev`してビルドに失敗しなければ問題ないはずです。

## GraphQL APIを作りデプロイする

続いて、API作っていきます。AmplifyではREST APIとGraphQL APIのどちらかが作れます。今回はGraphQLを使ってみましょう。

```bash
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: todoapi
? Choose the default authorization type for the API API key
? Enter a description for the API key: todo
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No
```

以上で、Graph QLのスキーマも自動で生成されます。ファイル内容を見てみましょう。

```graphql:/amplify/backend/api/todoapi/schema.graphql
type Todo @model {
  id: ID!
  name: String!
  description: String
}
```

`id`と`name`と`description`というデータが格納できるスキーマのモデルが作られてます。`@model`と指定されていますがこれは[Graph QL transform](https://docs.amplify.aws/cli/graphql-transformer/directives)ライブラリが用意するディレクティブです。これがあることにより、テーブルとCRUD機能が用意されます。つまり、APIの実装自体はもうほぼ完成だったりします。

なので、GraphQL APIをデプロイしましょう。

```bash
$ amplify push

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | todoapi       | Create    | awscloudformation |
? Are you sure you want to continue? Yes

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src\graphql\**\*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
```

これでAPIが作成されました。作成されたかを確認してみましょう。

```bash
$ amplify status

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | todoapi       | No Change | awscloudformation |

GraphQL endpoint: https://######################.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: ######################
```

そしてAWS Console上からも確認してみましょう。

```bash
$ amplify console
```

これで以下画面が開きます。何も表示されなかったらリージョンがオハイオとかになってる可能性あるので、右上のメニューから東京にしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/7d1d28a0-1a3a-1be4-f9e7-ffdc56dc6805.png)

これクリックしてbackendを選択して以下の画面で`View in AppSync`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/6995c401-5797-488c-17ba-8ad8a64a0262.png)


## フロントエンドとAPIを接続する

フロントのNuxtアプリからAPIを叩くようにページを用意しましょう。

```vue:/pages/index.vue
<template>
  <div>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import { createTodo } from '~/src/graphql/mutations'
import { listTodos } from '~/src/graphql/queries'

export default {
  data() {
    return {
      name: '',
      description: '',
      todos: [],
    }
  },
  async created() {
    await this.getTodos()
  },
  methods: {
    async createTodo() {
      const { name, description } = this
      if (!name || !description) return false
      const todo = { name, description }
      await API.graphql({
        query: createTodo,
        variables: { input: todo },
      })
      this.name = ''
      this.description = ''
      this.getTodos()
    },
    async getTodos() {
      const todos = await API.graphql({
        query: listTodos,
      })
      this.todos = todos.data.listTodos.items
    },
  },
}
</script>
```

このコードだと、初期のTODO取得と新規追加ができます。ですが、AmplifyのGraphQLはsubscriptionという機能を使ってリアルタイムでのデータ更新を実現できます。これはAPI作成時に自動で生成されているので、特に設定は不要で読み込む設定させすればできちゃいます。リアルタイムで同期するので、`createTodo`の末尾にある`this.getTodos()`は要らなくなります。

```vue:/pages/index.vue
<template>
  <div>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import { createTodo } from '~/src/graphql/mutations'
import { listTodos } from '~/src/graphql/queries'
import { onCreateTodo } from '~/src/graphql/subscriptions'

export default {
  data() {
    return {
      name: '',
      description: '',
      todos: [],
    }
  },
  created() {
    this.getTodos()
    this.subscribe()
  },
  methods: {
    async createTodo() {
      const { name, description } = this
      if (!name || !description) return false
      const todo = { name, description }
      await API.graphql({
        query: createTodo,
        variables: { input: todo },
      })
      this.name = ''
      this.description = ''
    },
    async getTodos() {
      const todos = await API.graphql({
        query: listTodos,
      })
      this.todos = todos.data.listTodos.items
    },
    subscribe() {
      API.graphql({ query: onCreateTodo }).subscribe({
        next: (eventData) => {
          const todo = eventData.value.data.onCreateTodo
          if (this.todos.some((item) => item.name === todo.name)) return // remove duplications
          this.todos = [...this.todos, todo]
        },
      })
    },
  },
}
</script>
```

## 認証機能を追加する

認証もAmplifyで追加できます。認証はAWSのCognitoを利用します。

```bash
$ amplify add auth
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. # 以下の設定は設定後の変更不可
 How do you want users to be able to sign in? Email
 Do you want to configure advanced settings? No, I am done.
```

これだけで準備完了です。後はサービスをデプロイするためにはpushしてあげる必要があります。

```bash
$ amplify push
? Are you sure you want to continue? Y
```

これでCognitoのセットアップが完了します。次はAmplifyが用意してくれてるVueコンポーネントを差し込んでみましょう。

```vue
<template>
  <amplify-authenticator>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
    <amplify-sign-out></amplify-sign-out>
  </amplify-authenticator>
</template>
```

`<amplify-authenticator>`コンポーネントの子ノードは認証が通った後に表示されるようになります。`<amplify-sign-out>`コンポーネントは、いわゆるサインアウトボタンを表示させるだけです。スタイルはドキュメント見た限り[カスタマイズできそう](https://docs.amplify.aws/ui/q/framework/vue)。以下はデフォルトの見た目です。英語です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/7e90b639-ad81-9547-7595-83eadc59f6a9.png)

サインインすると、子ノードの内容が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/2bc6e4da-b0fb-df4a-863c-1d2ee664286d.png)

認証がこれだけで出来ちゃいます。もちろんCognito側でのアカウント管理もできます。サインインについては提供されてるコンポーネントの他にライブラリ側に用意されたAPIを利用して独自の処理を追加できます。例えば、アカウント情報登録時に追加の情報を登録するなども実現できそうです。

## アプリをホスティング環境にデプロイする

AmplifyのCLI使ってデプロイするとS3上でホスティングしてくれるようになります。早速設定していきましょう。

```bash
$ amplify add hosting
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Manual deployment
```

これでもう設定OKです。ではホスティング環境にデプロイしてみましょう。

```bash
$ amplify publish

Current Environment: dev

| Category | Resource name       | Operation | Provider plugin   |
| -------- | ------------------- | --------- | ----------------- |
| Hosting  | amplifyhosting      | Create    | awscloudformation |
| Api      | todoapi             | No Change | awscloudformation |
| Auth     | nuxtamplify2271a807 | No Change | awscloudformation |
? Are you sure you want to continue? Yes

...

✔ Deployment complete!
https://dev.XXXXXXXXXXX.amplifyapp.com
```

このURLクリックしてもらうともうアプリに触れると思います。

## 次のステップ

わずか数時間でシンプルなアプリがもう実現できたかと思います。
冒頭の繰り返しになりますが、Amplifyでは以下がAmplifyの機能を使って簡単に実現できるので、ドキュメントは英語中心になってますが色々調べながら次のステップを踏みアプリを効率よく開発していきましょう！

- Authentication
- DataStore
- User File Storage
- Serverless APIs
- Analytics
- AI/ML
- Push Notification
- PubSub
- AR/VR

https://docs.amplify.aws/start/getting-started/nextsteps/q/integration/vue

## トラブルシューティング

### ビルド時にLinterで怒られる

Prettierの設定で怒られている可能性があります。`.prettierrc`ファイルの差し替えてみてください。

```json:.prettierrc
{
  "tabWidth": 2,
  "arrowParens": "always",
  "trailingComma": "es5",
  "printWidth": 80,
  "semi": false,
  "singleQuote": true,
  "bracketSpacing": true,
  "useTabs": false,
  "endOfLine": "auto
}
```

あるいは、VSCodeの拡張機能であるVeturを入れてる場合は、VeturのHTMLデフォルトフォーマッターが`prettyhtml`になっている可能性があります。ワークスペースの設定からフォーマッターを`prettier`に変更してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/58f22ef6-9c33-331f-e9bc-05123f956f04.png)