23
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-01

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

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

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

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

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

Nest CLIでNestJSのプロジェクト作成

NestJSのプロジェクトは、Nest CLIで簡単に作成できます。今回はtstodo-serverというプロジェクトを作成します。

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

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

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

Hello world

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

% cd tstodo-server 
% npm run start

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

スクリーンショット 2020-09-30 15.30.50.png

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

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

プロジェクトフォルダの中身

デフォルトでこんな構成になっています。

% 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直下に書いていきます。デフォルトでは以下。

% 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するところらしい。らしい
基本は触らないで良いですが、ポートがフロントとかぶるので変えます。

あと、CORSの設定用もここで追加しました。{ cors: true }
これを設定しないと、フロント側からアクセスできません。(実際デプロイするときは適切に設定したほうがよさそう

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

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

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クラスはここで定義されています。データベース等のやりとりも基本はここにメソッドを書けばよいっぽいです。

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をまとめたオブジェクトを渡します。

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://docs.nestjs.com/modules )

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

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

TypeOrmでデータベース操作

NestJSでデータベースを操作するのに、今回はTypeORMというORMを使用します。(まず、ORMって何?って感じでしたがこちらの記事がわかりやすかったです。ありがとうございます。)

NestJSではTypeORM用のパッケージが提供されており、ドキュメントの例でもtypeOrmでmySQLを使用する例が示されています。今回はこの例をベースに作成しました。

TypeORMのインストール

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

% npm install @nestjs/typeorm typeorm mysql

ormconfig.json

TypeORMの設定用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.modulesTypeOrmModuleをインポートし、@moduleの引数のオブジェクトにTypeOrmModule.forRoot()を追加します。

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.tsitemModuleクラスを作成します。手動で、ディレクトリとファイルを作成してもいいのですが、Nest CLIの便利コマンドがあるので、それを使っていきましょう。

% 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

↓ItemsModuleが追加されている。

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を記述します。

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デコレータを作成すれば、それをカラムにしたテーブルになります。

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ファイルを生成します。

% nest generate service items

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

生成したitemsService.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クエリを書かなくてもデータベースの操作を行うことができます。

今回は、findfindOneinsertupdatedeleteだけでこと足りました。(詳しい使い方や、他のメソッドについては、TypeORMのドキュメントをご参照ください)

ルーティング

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

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

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です。詳しくは:ドキュメント

items.modules.tsにserviceとcontrollerが勝手にインポートされている

items.service.tsitems.controller.tsを生成した際に、Nest CLIがitems.modules.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に追加します。

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

axiosの型定義の追加

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

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

index.vueの修正

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

  • Todoアイテムの操作を行うメソッドをaxiosを使ってデータベース操作をするよう書き直し
  • データベースからアイテムを全て取得するgetItemsメソッド追加
  • mounted時にgetItemメソッドを実行し全アイテムを取得
@/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モジュールの使い方はここが詳しかったです。getメソッドの代わりに$getメソッドを使えばさらにシンプルに書けたりします。

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

DockerでMySQLコンテナを用意

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

docker-compose.yml

docker hubのmysqlのイメージを流用してみます。
適当な(いい意味で)ディレクトリに以下のファイルを作成します。

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があるディレクトリで、以下を実行。

% docker-compose up -d

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

% npm run start:dev

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

% npm run dev

ブラウザでlocalhost:3000にアクセスします。
スクリーンショット 2020-10-01 16.25.33.png
動いてるっぽい。アイテム追加して、実行済みのチェックをつけてみます。
スクリーンショット 2020-10-01 16.26.46.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などにも触れられていい勉強になりました!!

コード

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

次は

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

あとがき

誰か仕事ください。

23
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?