この記事は、今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019 23日目の記事です。
導入
皆さんこんにちは。らいパン粉という者です。twitter→https://twitter.com/elipmoc101
さて、皆さんはバックエンドのプログラミング言語に何を選ぶでしょうか。
PHP?Ruby?JavaScript(Node.js)?
それともElixir、Scala、Go、Rust、Haskell等でしょうか。Coolですね。
最近、フロントエンドではTypeScriptが有名です。
この際、バックエンドもフロントエンドもTypeScriptでサクッと開発してみたいと思いません?僕は思いませんけど。(Ebio Syntax)
そんなわけで、TypeScriptでサクッとバックエンド開発ができるNestJSを紹介していきます。
let's npm start~
NestJSとは
- Typescript製のバックエンドフレームワーク
- デフォルトではExpressをコアとして動作
- Fastifyをコアとして動作させることもできる
- Node.jsで上で動く
- 実装と疎結合になるようなアーキテクチャ
- nest cli で簡単にプロジェクトやソースファイルのテンプレートを生成できる
- Expressのミドルウェアをそのまま使える
- テストフレームワークが用意されている
- 認証ライブラリはPassportなどが使える
- GraphQLもサポート
- WebSocketももちろんOK
- class-validatorを使って楽々バリデーション
- TypeORMで型の恩恵を最大限に受けつつDB操作できる
- とにかく拡張性が高い
まあ、一番良いと思うのは、TypeScriptである点とcliファイル生成してサクサク開発できるところですね。
公式サイト
https://docs.nestjs.com/
おすすめポイントピックアップ
Expressの便利なアレコレが使える
NestJSはデフォルトではExpressをコアとして動作します。
また、Expressの機能はNestJSでも使えます。
なので、Expressで動作するライブラリをそのまま活用できる可能性があります。
自動で差分コンパイルしてくれる
gulpとかtsc -w を使うまでもない!!
まず以下のコマンドを実行することで、サーバを開発モードで起動できます。
npm run start:dev
この状態で、ソースファイルを書き換えて保存すると、ファイル監視システムが検知して、差分をビルドしてサーバを再起動してくれます。
デコレータがお手軽
とりあえず分からない人向けにデコレータについて解説されたQiita記事置いときますね。
https://qiita.com/taqm/items/4bfd26dfa1f9610128bc
NestJSではデコレ―タというTypeScriptの言語機能を使って、様々な機能を簡単に実装できます。
HTTPリクエストに対する処理を例にしましょう。
@Controller('app')
export class AppController {
@Get('fuga')
getFuga(@Query() query: { text: string }): string {
return query.text
}
}
解説していきます。
まず@Controller('app')
です。
これはAppController
クラスがコントローラであることをNestJSに伝えています。
引数の'app'
はコントローラのurlパス名の指定です。
例では、localhost:3000/app
がAppController
の領域になります。
次に@Get('fuga')
の説明です。
これはgetFuga
関数がGetリクエストを処理することをNestJSに伝えています。
引数の'fuga'
はリクエストのurlパス名の指定です。
先ほどの'app'
に続けて追記されるので、localhost:3000/app/fuga
にGetリクエストすることで、getFuga
関数が実行されます。
次に@Query()
です。これを書いてから引数を取ると、URLクエリパラメータが引数に渡ってきます!
@Query() query: { text: string }
と書くことで、キーがtext
のクエリパラメータを取得できるわけです。
実際にアクセスするとこんな感じになります。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
ルーティングとコントローラが一体化してる
ルーティングとコントローラが分離されているデメリット
ルーティングとコントローラの処理が別ファイルとして分離していると、困ることがあります。
実際に具体例を見ていきましょう。
CASE.とあるRailsプロジェクトにて
ワイ「今日はhogefuga コントローラの修正やな。まあサクッと終わらせるで」
app/controllers/v1/hogefuga_controller.rb
ワイ「結構フォルダ階層深いんやな」
ワイ「これのindexメソッドを修正したらええんやな。その前に実際にブラウザでアクセスして動作確認してみるで」
ワイ(このプロジェクトは何故か単体テストが無いんや。だから動作確認は手動でするしかあらへん)
localhost:3000/v1/hogefuga
にアクセスする
ワイ「あれ?404エラーや。しゃーない。一回ルーティングの定義見に行くやで」
/app
/bin
/config
/db
/lib
/log
/public
/spec
/storage
/tmp
ワイ「あれルーティング定義してるファイルどこやったっけ?app
じゃないし、bin
じゃないし、db
でもないやろし」
ワイ「そもそもワイは何をしたかったんやっけ?(記憶消去)」
1分後
ワイ「ああ!config
のroutes.rb
やったわ!」
ワイ「うへぇ……routes.rb
ごちゃごちゃしててわかりずらいで」
1分後
ワイ「何とか見つけた……。v1/hoge/fuga/get_hogefuga
にGetリクエストや。なんでこんなurl設計になってんねん!」
ワイ「もはやRESTful APIじゃなくて、Stressful APIやな」
このように、ルーティング定義とコントローラが分かれていると、行き来するケースがあり大変です。
認知負荷が高いですね!!
これがルーティングとコントローラが分かれているデメリットです。(もちろん、ルーティングの責務をコントローラから分離するとメリットもありますので、トレードオフですが)
NestJSでは、コントローラでルーティングを定義してしまいます。
import { Controller, Get } from '@nestjs/common';
@Controller('v1/hoge/fuga')
export class HogefugaController {
@Get('get_hogefuga')
index() {
return 'hogehogefugafuga'
}
}
すでに@Controller
と@Get
については説明済みなので、説明は省略します。
アーキテクチャがいい感じ
これは局所的な例なので、必ずしもこうとは限りませんが、NestJSでは上の図のような構成で開発をすることが可能です。(図の意味が分からない人はクラス図を勉強してください)
依存関係の分離とかはDIコンテナがいい感じにやってくれます。
DIコンテナについて解説したQiita記事置いときます。
https://qiita.com/hinom77/items/1d7a30ba5444454a21a8
https://qiita.com/ritukiii/items/de30b2d944109521298f
各責務を紹介します
- Module
- 依存関係を管理する責務
- Controller
- ユーザからのリクエストに答える責務
- 実際の処理はServiceに任せる
- Service
- 各種機能をControllerに提供する責務
- Repository
- データの永続化をする責務
以下は実装の例です。
この設計のメリットは、主要ロジックがインフラストラクチャに依存することを防ぐことができるところでしょうか。
テストが楽
NestJSでは、単体テストはかなり楽にできます。
まずcliでコントローラやサービスなどを生成していくわけですが、同時にテストファイルも生成してくれるので最高です。
さらに、アーキテクチャやDIコンテナのおかげでリポジトリやサービスの実装などをテスト用にすり替えるのが非常に簡単になっています。
インストール~プロジェクト作成~DB操作まで
手を動かしてやってみた方が速い!!
というわけで一通り、やってみましょう!
まずは適当にフォルダを作ります。
今回はnest_test
という空フォルダを作り、そこを利用します。
-g
でnpm install
はしないので、そこはご了承ください。
インストールとプロジェクト作成
cd nest_test
npm i @nestjs/cli
npx nest new .
nest_test
フォルダに移動したら、NestJS用のCliをインストールします。
nest new [プロジェクトフォルダ名]
でプロジェクトを作成できるのですが、.
を入力することで、nest_test
をそのままプロジェクトとして作成します。
nest new
の詳細な情報
https://docs.nestjs.com/cli/usages#nest-new
サーバの起動
npm run start:dev
このコマンドを打つことによって開発用サーバが起動します。
デフォルトではlocalhost:3000
になります。
アクセスするとこうなります。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
これからは、サーバを停止した状態を前提として解説するので注意してください。
(暗黙的な自動コンパイルは教える際の障害となるため)
TypeORMとSqliteのセットアップ
DBを操作するためのライブラリとしてTypeORMを使っていきます。
DBはお手軽にSQLiteにしました。
必要なものをインストールしていきます。
npm i --save @nestjs/typeorm typeorm sqlite3
そしてTypeORMにDBの情報を教えるため、TypeORMの設定ファイルを用意します。
プロジェクト直下にormconfig.json
というファイルを作成しましょう。
{
"type": "sqlite",
"database": "data/dev.sqlite",
"entities": [
"dist/entities/**/*.entity.js"
],
"migrations": [
"dist/migrations/**/*.js"
],
"logging": true
}
"type"
にはDBの種類を指定します。
"mysql"
, "postgres"
, "cockroachdb"
, "mariadb"
, "sqlite"
, "oracle"
, "mssql"
, "mongodb"
, "sqljs"
など、指定できるDBはかなり多いです。
https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#connection-options-example
"database"
にはDBの名前を指定します。
"entities"
にはテーブル定義となるEntityが定義されたjsがある場所を指定します。(Entityについては後述)
今回はdist/entities
配下にEntityが定義されたjsを生成するようにします。
"migrations"
にはマイグレーションファイルがある場所を指定します。
今回はdist/migrations
配下にマイグレーションファイルのjsを生成するようにします。
"logging"
をtrue
に設定することで、実行したSQL文をLogとして確認できるようになります。これはデバックする際の良いヒントとなります。
次にNestJSにTypeORMを組み込んでいきます。app.module.ts
に追記します。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm'; //追加!
@Module({
imports: [TypeOrmModule.forRoot()], //追加!
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
この設定により、全てのモジュールからTypeORMの機能を呼び出せるようになります。
Entityを定義
テーブルを作成するため、Entityを1つ定義してみます。
TypeORMにおけるEntityとは、テーブルのデータ構造を、クラス構文で表現したオブジェクトのことです。
Entityを作成することで、テーブルの定義を行うことができます。
まずnest_test/src/entities/memo.entity.ts
ファイルを生成します。
そして以下のようにコードを書いてください。
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Memo {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 500 })
name: string;
@Column('text')
description: string;
}
@Entity()
を付けたクラスはEntityとなり、memoテーブルの定義となります。
@PrimaryGeneratedColumn()
をクラスの変数に付けると、それは自動増分主キーのカラムとなります。
@Column()
をクラスの変数に付けると、それはカラムとして登録され、@column('text')
のように属性を指定したりもできます。
これで、Entityの作成完了です。
マイグレーションする
マイグレーションを行い、DBにテーブルを作成していきたいと思います。
まずはマイグレーションファイルを作成します。
先ほど作ったEntityからマイグレーションファイルを自動生成することができるので、それをやります。
npm run build
npx typeorm migration:generate -d src/migrations -n create-memo
npm run build
でdist/entities
配下にmemo.entity.js
を出力し、TypeORMのコマンドで、create-memo
という名前でマイグレーションファイルを作成しています。
生成されたマイグレーションファイルはこんな感じです。
import {MigrationInterface, QueryRunner} from "typeorm";
export class createMemo1575868486667 implements MigrationInterface {
name = 'createMemo1575868486667'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "memo" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(500) NOT NULL, "description" text NOT NULL)`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`DROP TABLE "memo"`, undefined);
}
}
次にマイグレーションファイルの実行をします。
npm run build
npx typeorm migration:run
動作としては、dist/migrations
に生成されたマイグレーションファイルを実行しているだけです。
モジュール、コントローラ、サービス、リポジトリの実装
次にMemoに関するモジュール、コントローラ、サービスを作っていきます。
npx nest g mo memo
npx nest g co memo
npx nest g s memo
nest g
の詳細な情報
https://docs.nestjs.com/cli/usages#nest-generate
nest_test/src/memo
フォルダ配下に以下の三つのファイルが生成されていることが確認できると思います。
- memo.module.ts
- memo.controller.ts
- memo.service.ts
次にMemoテーブルを操作をするリポジトリを実装and登録します。
なんと!TypeORMがリポジトリの実装を自動でやってくれます!(さらに実装されたリポジトリを継承してカスタムリポジトリも作れるらしい https://docs.nestjs.com/techniques/database#custom-repository)
memo.module.ts
に追記します。
import { Module } from '@nestjs/common';
import { MemoService } from './memo.service';
import { Memo } from 'src/entities/memo.entity'; //追加!
import { TypeOrmModule } from '@nestjs/typeorm'; //追加!
import { MemoController } from './memo.controller';
@Module({
controllers: [MemoController],
imports: [TypeOrmModule.forFeature([Memo])], // 追加!
providers: [MemoService]
})
export class MemoModule { }
Memoサービスに登録したリポジトリを注入します。
コンストラクタの引数で@InjectRepository(Memo)
と書けば、DIコンテナが、登録されたMemoリポジトリを生成して渡してくれます。
import { Injectable } from '@nestjs/common';
import { Memo } from 'src/entities/memo.entity'; //追加!
import { Repository } from 'typeorm'; //追加!
import { InjectRepository } from '@nestjs/typeorm'; //追加!
@Injectable()
export class MemoService {
constructor(
@InjectRepository(Memo) //追加!
private readonly memoRepository: Repository<Memo> //追加!
) { }
}
Memoサービスを実装していきます。
import { Injectable } from '@nestjs/common';
import { Memo } from 'src/entities/memo.entity';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class MemoService {
constructor(
@InjectRepository(Memo)
private readonly memoRepository: Repository<Memo>
) { }
addMemo(name: string, description: string) {
const memo = new Memo()
memo.name = name;
memo.description = description;
return this.memoRepository.insert(memo);
}
getMemoList() {
return this.memoRepository.find();
}
}
MemoコントローラーにMemoサービスを注入してMemoサービスの関数を利用し、Get・Postリクエストを実装していきます。
コチラもDIコンテナのおかげでMemoService
型の引数があれば、勝手に生成して渡してくれます。
DIコンテナは本当に素晴らしいですね!我々は生成の責務を考えないで実装に専念できます!!
import { Controller, Get, Post, Query } from '@nestjs/common';
import { MemoService } from './memo.service';
@Controller('memo')
export class MemoController {
constructor(private readonly service: MemoService) { }
@Get()
getMemoList() {
return this.service.getMemoList();
}
@Post()
addMemo(@Query() query: { name: string, description: string }) {
return this.service.addMemo(query.name, query.description);
}
}
動作確認
npm run start:dev
localhost:3000/memo
にGetリクエストすると、空の配列が返ってきます。
localhost:3000/memo?name=fuga&description=hogehoge
にPostリクエストし、再度localhost:3000/memo
にGetリクエストすると、memoが一つ追加されているのが確認できます。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
結論
これからはExpressを捨ててNestJSの時代かも?!
あと、書いてから気づいたんですが、NestJSのアドベントカレンダーありました。(NestJSのマニアックな話知りたい人は是非)
https://qiita.com/advent-calendar/2019/nestjs
#参考
NestJS公式サイト
https://docs.nestjs.com/
TypeORM公式サイト
https://typeorm.io/
TypeORMのREADME
https://github.com/typeorm/typeorm/blob/master/README.md
Qiita Nest.jsは素晴らしい
https://qiita.com/kmatae/items/5aacc8375f71105ce0e4
TypeScriptによるデコレータの基礎と実践
https://qiita.com/taqm/items/4bfd26dfa1f9610128bc
DIとDIコンテナを3分で理解する
https://qiita.com/hinom77/items/1d7a30ba5444454a21a8
DI・DIコンテナ、ちゃんと理解出来てる・・?
https://qiita.com/ritukiii/items/de30b2d944109521298f