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/ )
このままだと、ページ更新したらまた元に戻ってしまうので、データベースに状態を保存できるように作っていきます。
機能としては、以下の通り、基本的な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
が指定されています。
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 }
これを設定しないと、フロント側からアクセスできません。(実際デプロイするときは適切に設定したほうがよさそう)
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
コントローラ。ルーティングと、リクエスト、レスポンスの設定をするのがここです。
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
クラスはここで定義されています。データベース等のやりとりも基本はここにメソッドを書けばよいっぽいです。
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
をまとめたオブジェクトを渡します。
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
はその頂点となるモジュールになります。
↑ドキュメントより引用。
(公式ドキュメント: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/typeorm
とtypeorm
とmysql
全部をインストールします。
% npm install @nestjs/typeorm typeorm mysql
ormconfig.json
TypeORMの設定用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()
を追加します。
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の便利コマンドがあるので、それを使っていきましょう。
% 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が追加されている。
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を記述します。
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
デコレータを作成すれば、それをカラムにしたテーブルになります。
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
を下記のように編集します。
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のドキュメントをご参照ください)
ルーティング
最後にItemsControlにルーティングを記述します。serviceの場合と同様にnest generate controller items
して、items.controller.ts
を生成します。
/items/
以下にルーティングを作成して、先ほど記述したItemsService
クラスのメソッドを呼び出すようにしていきます。
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.ts
とitems.controller.ts
を生成した際に、Nest CLIがitems.modules.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に追加します。
export default {
(略)
modules: [
'@nuxtjs/axios',
],
(略)
}
axiosの型定義の追加
TypeScirptでaxiosモジュールを利用するには、tsconfig.json
に型定義を追加する必要があります。
{
"compilerOptions": {
(略)
"types": [
"@types/node",
"@nuxt/types",
"@nuxtjs/axios"
]
},
(略)
}
index.vueの修正
@/pages/index.vue
を以下のように修正します。
- Todoアイテムの操作を行うメソッドをaxiosを使ってデータベース操作をするよう書き直し
- データベースからアイテムを全て取得する
getItems
メソッド追加 -
mounted
時にgetItem
メソッドを実行し全アイテムを取得
<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のイメージを流用してみます。
適当な(いい意味で)ディレクトリに以下のファイルを作成します。
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
にアクセスします。
動いてるっぽい。アイテム追加して、実行済みのチェックをつけてみます。
ここで、ちゃんと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化したり、ログイン機能とかつけてデプロイすることに挑戦します。駆け出しの冒険は続く。
あとがき
誰か仕事ください。