Nuxt.jsとNestJSでTypeScriptなTodoリストを作ってみる（バックエンド編）

Vue.jsとTypeScriptの練習のために、Nuxt.jsとNestJSでTodoリストを作ってみました。
前回Nuxt.jsで作ったフロント側でデータベースからアイテムを生成、読み取り、更新、削除（CRUDって言うんですね）できるようにバックエンド側を作っていきます。データベースサーバはDockerで用意します。

(前編（フロント編）：https://qiita.com/daitai-daidai/items/36d44f7caaee4841ef34 )

使用技術は以下の通り。

- フロントエンド： Nuxt.js (UIフレームワークはVuetify、言語はTypeScript）
- バックエンド： NestJS + typeOrm + mySQL(サーバーはdockerで用意)

TypeScriptでできたサーバーサイドのフレームワークであるNestJSを利用します。日本語のドキュメントがないのが少ししんどいけど僕は英語完全に理解してるのでどーってことありませんでした。（しんどかった。）　細かく書いてあるのでありがたかったです。

公式ドキュメント：https://docs.nestjs.com/

### 環境
- MacOS Catalina v10.15.6
- Node.JS v14.10.1
- Nuxt.js v2.14.5
- NestJS v6.14.8
- Nest CLI v7.0.0
- TypeORM v0.2.27
- Docker v19.03.12
- docker-compose v1.27.2

## 今回目指すところ
前回作ったページがこんな感じ。（https://dai65527.github.io/nuxttodo/ ）
![スクリーンショット 2020-09-29 17.14.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/8125539e-836d-ffce-71d6-8b02262b5643.png)

このままだと、ページ更新したらまた元に戻ってしまうので、データベースに状態を保存できるように作っていきます。

機能としては、以下の通り、基本的なCRUD機能を実装します。

- Create: Todoアイテムの作成。
- Read: 作成済Todoアイテムの読み取り。
- Update: Todoアイテムの状態更新（実行済 or not）。
- Delete: Todoアイテムの削除。実行済アイテムの一斉削除。

なお、今回はローカル環境のみで動かすところまでにします。

## Nest CLIでNestJSのプロジェクト作成
NestJSのプロジェクトは、[Nest CLI](https://docs.nestjs.com/cli/overview)で簡単に作成できます。今回は`tstodo-server`というプロジェクトを作成します。

```zsh
% npm i -g @nestjs/cli
% nest new tstodo-server
```

npmかyarnかを聞かれるだけで、プロジェクトのフォルダが生成されます。
（初学者にはcreate-nuxt-appほど怖くなくてよいです。逆に必要なものは自分でインストールします。）

Nest CLIをインストールするのが嫌ならnpxを使ってもOKぽいです。
ただ、この後使うと便利になるので、インストールしちゃうことをおすすめします。

### Hello world
既に動かせる状態になっているので、とりあえず動かしてみます。

```zsh
% cd tstodo-server 
% npm run start
```

デフォルトでは`localhost:3000`が指定されています。

![スクリーンショット 2020-09-30 15.30.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/e10fa3b3-1ef5-9bab-42f6-e3900fb8aaa2.png)

`curl`コマンドを使うとより玄人っぽい（駆け出し並感）

```zsh
% curl http://localhost:3000/
Hello World!
```

## プロジェクトフォルダの中身
デフォルトでこんな構成になっています。

```zsh
% tree tstodo-server -L 1    
tstodo-server
├── README.md
├── dist
├── nest-cli.json
├── node_modules
├── package-lock.json
├── package.json
├── src
├── test
├── tsconfig.build.json
└── tsconfig.json
```

メインのコードは基本`src`直下に書いていきます。デフォルトでは以下。

```zsh
% tree tstodo-server/src 
tstodo-server/src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts
```

### main.ts
main.tsがNestのアプリケーションをCreateするところらしい。[らしい](https://docs.nestjs.com/first-steps)。
基本は触らないで良いですが、ポートがフロントとかぶるので変えます。

あと、CORSの設定用もここで追加しました。`{ cors: true }`
これを設定しないと、フロント側からアクセスできません。（実際デプロイするときは[適切に設定したほうがよさそう](https://qiita.com/att55/items/2154a8aad8bf1409db2b)）

```ts:src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });
  await app.listen(4000);   //default 3000
}
bootstrap();
```

(参考にさせていただきました: https://note.com/daitasu/n/nd85a1c72350a)

### app.control.ts
コントローラ。ルーティングと、リクエスト、レスポンスの設定をするのがここです。

```ts:src/app.control.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
```

`@Controller()`デコレータでルーティングの設定、`@get`デコレータでhttpリクエストメソッドの設定ができます。デフォルトの例だとルート`/`にGETリクエストを送ると、`this.appService.getHello()`の返り値をレスポンスとして返す。ってことになります。
`@Controller()`と`@get()`の引数を指定することで、ルーティングを変えられます。例えば、`@Controller(hoge)`、`@get(fuga)`に変更した場合、`/hoge/fuga/`にGETリクエストした場合getHello()が帰ってきます。

（公式ドキュメント：https://docs.nestjs.com/controllers )

ルートURLについてはとりあえず今回は使わないので、このまま残しておきます。

### app.service.ts
コントローラで呼び出されていた`appService`クラスはここで定義されています。データベース等のやりとりも基本はここにメソッドを書けばよいっぽいです。

```ts:src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
```

こういった`service`などのクラスをNestJSではプロバイダというらしい。`@injectable()`デコレータをつけて宣言すれば、プロバイダとして扱われます。（https://docs.nestjs.com/providers ）

### app.modules.ts
プロバイダとコントローラをまとめて、モジュール化しているのが`modules`です。`@modules`デコレータの引数に`controllers`やら`providers`をまとめたオブジェクトを渡します。

```ts:src/app.modules.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

`imports`は現在空になっていますが、ここには別のモジュールを加えることによって、NestJSの構造が作られていき、`app.modules`はその頂点となるモジュールになります。
![スクリーンショット 2020-09-30 19.07.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/191176f1-50a2-c622-c5dd-8144b74034ab.png)
↑[ドキュメント](https://docs.nestjs.com/modules)より引用。

(公式ドキュメント：https://docs.nestjs.com/modules )

基本機能はこんな感じで作れるみたいです。このままappモジュールに処理を記述していってもいいですが、今回は、Todoアイテムを操作する機能を持ったitemsモジュール（`item.modules.ts`）を作成しapp.modulesに追加していきます。

（なお、`src`ディレクトリには`app.controller.spec.ts`というテスト用ファイルがありますが、本記事では割愛します。）

## TypeOrmでデータベース操作
NestJSでデータベースを操作するのに、今回は[TypeORM](https://typeorm.io/)というORMを使用します。（まず、ORMって何？って感じでしたが[こちらの記事](https://qiita.com/niisan-tokyo/items/156eb35c6eeaf07b9b65)がわかりやすかったです。ありがとうございます。）

NestJSではTypeORM用のパッケージが提供されており、[ドキュメントの例](https://docs.nestjs.com/techniques/database)でもtypeOrmでmySQLを使用する例が示されています。今回はこの例をベースに作成しました。

### TypeORMのインストール
プロジェクトにTypeORMをインストールします。NestJS用のパッケージ`@nestjs/typeorm`と`typeorm`と`mysql`全部をインストールします。

```zsh
% npm install @nestjs/typeorm typeorm mysql
```

### ormconfig.json
TypeORMの設定用JSONをプロジェクトのルートに作成します。

```json:ormconfig.json
{
    "type": "mysql",
    "host": "localhost",
    "port": 3306,
    "username": "root",
    "password": "password",
    "database": "tstodo",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "synchronize": true
}
```

### app.modulesにTypeOrmModuleをインポート
TypeORMを使うために`app.modules`に`TypeOrmModule`をインポートし、`@module`の引数のオブジェクトに`TypeOrmModule.forRoot()`を追加します。

```ts:src/app.modules.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';  // TypeOrmModuleをインポート

@Module({
  imports: [TypeOrmModule.forRoot()],   // TypeOrmModule.forRoot()を追加
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

これでTypeORMを使う準備は整いました！！

## Todoアイテム操作の機能の実装
前置きが長くなっちゃいましたが、肝心のTodoアイテムを操作する機能（CRUD）の実装をしていきます。
今回は`items.*.ts`というtsファイルを`items`ディレクトリ以下に作成し、Itemsモジュールの中身を作っていきます。

### itemsModule
まず、`items.module.ts`に`itemModule`クラスを作成します。手動で、ディレクトリとファイルを作成してもいいのですが、Nest CLIの便利コマンドがあるので、それを使っていきましょう。

```zsh
% nest generate module items
CREATE src/items/items.module.ts (82 bytes)
UPDATE src/app.module.ts (460 bytes)
```

これだけで、

- `items`ディレクトリの作成
- `items.module.ts`の作成
- `app.modules.ts`の更新（インポート文の追加）

を自動でやってくれます。楽！！ （ちなみに`nest ge mo items`でもOKです。詳しくは:[Nest CLI](https://docs.nestjs.com/cli/usages#nest-generate)）

↓ItemsModuleが追加されている。

```ts:src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ItemsModule } from './items/items.module'; //ItemsModuleがインポートされている

@Module({
  imports: [TypeOrmModule.forRoot(), ItemsModule],   // ItemsModuleが追加されている
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

この段階では`items.modules.ts`はまだ空っぽの雛形だけが用意されている状態です。ここにTypeORMのモジュールのimportを記述します。

```ts:src/items/items.modules.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 追加
import { Item } from "./item.entity";

@Module({})
export class ItemsModule {
  imports: [TypeOrmModule.forFeature([Item])] // 追加
}
```

ひとまずこいつはこれでOK。

### エンティティでカラムを設定する
続いては、itemsディレクトリに`items.entity.ts`を作成して、Todoアイテムのエンティティを作成します。これに基づいて、TypeORMがデータベースに勝手にテーブルを作成してくれます。便利。
`@Entitiy`デコレータをつけてクラスを宣言します。そして、各メンバに`@Column`デコレータを作成すれば、それをカラムにしたテーブルになります。

```ts:src/items/item.entity
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Item {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ type: "boolean", default: false })
  done: boolean;
}
```

カラムの属性もここで記述します。
まず、`id`についている`@PrimaryGeneratedColumn`デコレータですが、これでプライマリーキーとし、かつオートインクリメントするということが指定できます。なお、各エンティティは必ず一つはプライマリーカラムを作成する必要があるようです。（単に`@PrimaryColumn`というデコレータもあります）
また、`done`のデコレータの引数では、型とデフォルト値を設定しています。

この内容だと以下のようなテーブルが作成されます。`boolean`型を指定すると`tinyint`になるんですね。（そもそもSQLにboolean型と言うのはないらしい）

```
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int          | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
| done  | tinyint      | NO   |     | 0       |                |
+-------+--------------+------+-----+---------+----------------+
```

### データベース操作
続いて、データベースの操作を記述するメソッドを含んだ`ItemsService`クラスを作成します。Nest CLIで`items.service.ts`ファイルを生成します。

```zsh
% nest generate service items
```

このとき、`items`ディレクトリ以下に、`items.Service.ts`とともに`items.Service.spec.ts`というテスト用のファイルも生成されますが、今回は扱いません。（`--no-spec`オプションを付ければ生成されません。）

生成した`itemsService.ts`を下記のように編集します。

```ts:src/items/items.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Item } from "./item.entity";

@Injectable()
export class ItemsService {
  constructor(
    @InjectRepository(Item)
    private itemsRepository: Repository<Item>,
  ) {}

  findAll(): Promise<Item[]> {
    return this.itemsRepository.find();
  }

  async addItem(name: string): Promise<void> {
    await this.itemsRepository.insert({name});
  }

  async changeDone(id: string): Promise<void> {
    const item = await this.itemsRepository.findOne(id);
    await this.itemsRepository.update(id, { done: !item.done });
  }

  async deleteItem(id: string): Promise<void> {
    await this.itemsRepository.delete(id);
  }

  async deleteDone(): Promise<void> {
    await this.itemsRepository.delete({done: true})
  }
}
```

TypeORMの`Repository`クラスのメソッドを使えば、SQLクエリを書かなくてもデータベースの操作を行うことができます。

今回は、`find`、`findOne`、`insert`、`update`、`delete`だけでこと足りました。（詳しい使い方や、他のメソッドについては、[TypeORMのドキュメント](https://typeorm.io/#/repository-api)をご参照ください）

### ルーティング
最後にItemsControlにルーティングを記述します。serviceの場合と同様に`nest generate controller items`して、`items.controller.ts`を生成します。

`/items/`以下にルーティングを作成して、先ほど記述した`ItemsService`クラスのメソッドを呼び出すようにしていきます。

```ts:src/items/items.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ItemsService } from "./items.service";
import { Item } from "./item.entity";

@Controller('items')
export class ItemsController {
  constructor(private itemsService: ItemsService) {}

  @Get()
  findAll(): Promise<Item[]> {
    return this.itemsService.findAll();
  }

  @Post()
  async addItem(@Body() item: { name: string }) {
    return await this.itemsService.addItem(item.name);
  }

  @Put('done/:id')
  async changeDone(@Param('id') id: string): Promise<void> {
    return await this.itemsService.changeDone(id);
  }

  @Delete(':id')
  async deleteOne(@Param('id') id: string): Promise<void> {
    return await this.itemsService.deleteItem(id);
  }

  @Delete()
  async deleteDone(): Promise<void> {
    return await this.itemsService.deleteDone();
  }
}
```

ルートパラメータは`@Param`デコレータ、POSTで送られてくるデータは`@Body`デコレータを使って取得できます。（ちなみに、GETのデータは`@Query`です。詳しくは：[ドキュメント](https://docs.nestjs.com/controllers#request-object)）

### items.modules.tsにserviceとcontrollerが勝手にインポートされている
`items.service.ts`と`items.controller.ts`を生成した際に、Nest CLIが`items.modules.ts`に勝手にインポートしてくれています。有能。

```ts:src/items/items.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Item } from "./item.entity";
import { ItemsService } from './items.service';       // 勝手にインポート
import { ItemsController } from './items.controller'; // 勝手にインポート

@Module({
  imports: [TypeOrmModule.forFeature([Item])],
  providers: [ItemsService],        // 勝手に追加
  controllers: [ItemsController]    // 勝手に追加
})
export class ItemsModule {}
```

これで、NestJS側の作成は終了です。

## フロント(Nuxt.js)側の修正
前回作成したフロント側をnuxt.jsのaxiosモジュール`@nuxtjs/axios`を使ってhttp通信で取得できるようにします。
前回、create-nuxt-appでaxiosを選択されていれば、`@nuxtjs/axios`が既にインストールされているはずです。

されていない場合は、

```
% npm install @nuxtjs/axios
```

して、`nuxt.config.ts`でmodulesに追加します。

```ts:@/nuxt.config.ts
export default {
  (略)
  modules: [
    '@nuxtjs/axios',
  ],
  (略)
}
```


### axiosの型定義の追加
TypeScirptでaxiosモジュールを利用するには、`tsconfig.json`に型定義を追加する必要があります。

```json:@/tsconfig.json
{
  "compilerOptions": {
    (略)
    "types": [
      "@types/node",
      "@nuxt/types",
      "@nuxtjs/axios"
    ]
  },
  (略)
}
```

### index.vueの修正
`@/pages/index.vue`を以下のように修正します。

- Todoアイテムの操作を行うメソッドをaxiosを使ってデータベース操作をするよう書き直し
- データベースからアイテムを全て取得する`getItems`メソッド追加
- `mounted`時に`getItem`メソッドを実行し全アイテムを取得

```ts:@/pages/index.vue
<template>
  <v-container class="pt-10">
    <List
      :items="items"
      @add-item="addItem"
      @change-done="changeDone"
      @delete-item="deleteItem"
      @delete-done="deleteDone"
    />
  </v-container>
</template>

<script lang="ts">
import Vue from 'vue'
import Item from '@/models/Item'
import List from '@/components/List.vue'

export default Vue.extend({
  components: {
    List,
  },
  data: (): { items: Item[] } => ({
    items: [],
  }),
  async created() {
    await this.getItems()
  },
  methods: {
    async getItems() {
      const res = await this.$axios.get('http://localhost:4000/items/')
      this.items = res.data
    },
    async addItem(itemName: string) {
      await this.$axios.post(`http://localhost:4000/items/`, {
        name: itemName,
      })
      await this.getItems()
    },
    async changeDone(id: number) {
      await this.$axios.put(`http://localhost:4000/items/done/${id}/`)
    },
    async deleteItem(id: number) {
      await this.$axios.delete(`http://localhost:4000/items/${id}/`)
      await this.getItems()
    },
    async deleteDone() {
      await this.$axios.delete(`http://localhost:4000/items/`)
      await this.getItems()
    },
  },
})
</script>
```

axiosは`this.$axios`で呼び出せます。axiosモジュールの使い方は[ここ](https://axios.nuxtjs.org/usage)が詳しかったです。`get`メソッドの代わりに`＄get`メソッドを使えばさらにシンプルに書けたりします。

これで、フロント側の修正は終わりです。

## DockerでMySQLコンテナを用意
続いてデータベースを用意します。
せっかくなので、Dockerの勉強もしてみようと思い、MySQLのコンテナを用意します。

### docker-compose.yml
[docker hubのmysqlのイメージ](https://hub.docker.com/_/mysql)を流用してみます。
適当な（いい意味で）ディレクトリに以下のファイルを作成します。

```yaml:docker-compose.yml
version: "3.1"
services:
  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: tstodo
    ports:
      - 3306:3306
```

`MYSQL_DATABASE: tstodo`を追加してやれば、コンテナ作成時に勝手にテーブル作ってくれます。というわけで、これだけでOK。

docker-composeについてはここがわかりやすかったです。（https://futureys.tokyo/lets-run-the-web-application-for-development-by-docker-desktop-and-access-it-by-browser/　）

## 動かしてみる
必要なものが一通り揃ったので動かしてみます。

まず、先ほど作った`docker-compose.yml`があるディレクトリで、以下を実行。

```zsh
% docker-compose up -d
```

続いて、`tstodo-server`のディレクトリで以下を実行。

```zsh
% npm run start:dev
```

そして、`tstodo-client`のディレクトリで以下を実行。

```zsh
% npm run dev
```

ブラウザで`localhost:3000`にアクセスします。
![スクリーンショット 2020-10-01 16.25.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/9e92a0fc-71e6-2cc2-8859-5f3f926e50fa.png)
動いてるっぽい。アイテム追加して、実行済みのチェックをつけてみます。
![スクリーンショット 2020-10-01 16.26.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/85466fbc-ed29-3bba-825c-8ca79c3641fd.png)
ここで、ちゃんとMySQLにテーブルができているか確認してみましょう。（コンテナIDは`docker ps`コマンドで確認できます）

```
% docker exec -it <コンテナID> /bin/bash
# mysql -uroot -p

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| tstodo             |
+--------------------+
5 rows in set (0.01 sec)

mysql> use tstodo;
mysql> describe item;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int          | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
| done  | tinyint      | NO   |     | 0       |                |
+-------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> select * from item;
+----+--------+------+
| id | name   | done |
+----+--------+------+
|  1 | item 1 |    0 |
|  2 | item2  |    1 |
|  3 | item3  |    0 |
+----+--------+------+
3 rows in set (0.00 sec)
```

正常に動いていそうです！！ アイテムの追加、削除も動きます。もちろん更新しても元どおりなんてことはありません。

## まとめ
NestJSを使ってTodoリストのバックエンド側を作成しました。TypeORMやDockerなどにも触れられていい勉強になりました！！

### コード
- フロントエンド： https://github.com/dai65527/tstodo-client
- バックエンド： https://github.com/dai65527/tstodo_server
- 完成品（こちらは見た目だけ）: https://dai65527.github.io/nuxttodo/

ここ変だぞ？ とか、ここもっとよくなるぞ？ ってとことかありましたらコメント等で教えていただけますと幸いです！！

### 次は

開発環境を全てDocker化したり、ログイン機能とかつけてデプロイすることに挑戦します。駆け出しの冒険は続く。

### あとがき
誰か仕事ください。