LoginSignup
548
414

More than 3 years have passed since last update.

この記事は、今年イチ!お勧めしたいテクニック 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/appAppControllerの領域になります。
次に@Get('fuga')の説明です。
これはgetFuga関数がGetリクエストを処理することをNestJSに伝えています。
引数の'fuga'はリクエストのurlパス名の指定です。
先ほどの'app'に続けて追記されるので、localhost:3000/app/fugaGetリクエストすることで、getFuga関数が実行されます。
次に@Query()です。これを書いてから引数を取ると、URLクエリパラメータが引数に渡ってきます!
@Query() query: { text: string }と書くことで、キーがtextのクエリパラメータを取得できるわけです。
実際にアクセスするとこんな感じになります。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
image.png
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

ルーティングとコントローラが一体化してる

ルーティングとコントローラが分離されているデメリット

ルーティングコントローラの処理が別ファイルとして分離していると、困ることがあります。
実際に具体例を見ていきましょう。

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分後
ワイ「ああ!configroutes.rbやったわ!」
ワイ「うへぇ……routes.rbごちゃごちゃしててわかりずらいで」
1分後
ワイ「何とか見つけた……。v1/hoge/fuga/get_hogefugaGetリクエストや。なんでこんな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については説明済みなので、説明は省略します。

アーキテクチャがいい感じ

a.png

これは局所的な例なので、必ずしもこうとは限りませんが、NestJSでは上の図のような構成で開発をすることが可能です。(図の意味が分からない人はクラス図を勉強してください)

依存関係の分離とかはDIコンテナがいい感じにやってくれます。
DIコンテナについて解説したQiita記事置いときます。
https://qiita.com/hinom77/items/1d7a30ba5444454a21a8
https://qiita.com/ritukiii/items/de30b2d944109521298f

各責務を紹介します

  • Module
    • 依存関係を管理する責務
  • Controller
    • ユーザからのリクエストに答える責務
    • 実際の処理はServiceに任せる
  • Service
    • 各種機能をControllerに提供する責務
  • Repository
    • データの永続化をする責務

以下は実装の例です。

b.png

この設計のメリットは、主要ロジックがインフラストラクチャに依存することを防ぐことができるところでしょうか。

テストが楽

NestJSでは、単体テストはかなり楽にできます。
まずcliでコントローラサービスなどを生成していくわけですが、同時にテストファイルも生成してくれるので最高です。
さらに、アーキテクチャやDIコンテナのおかげでリポジトリサービスの実装などをテスト用にすり替えるのが非常に簡単になっています。

インストール~プロジェクト作成~DB操作まで

手を動かしてやってみた方が速い!!
というわけで一通り、やってみましょう!

まずは適当にフォルダを作ります。
今回はnest_testという空フォルダを作り、そこを利用します。
-gnpm 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になります。
アクセスするとこうなります。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
image.png
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

これからは、サーバを停止した状態を前提として解説するので注意してください。
(暗黙的な自動コンパイルは教える際の障害となるため)

TypeORMとSqliteのセットアップ

DBを操作するためのライブラリとしてTypeORMを使っていきます。
DBはお手軽にSQLiteにしました。
必要なものをインストールしていきます。

npm i --save @nestjs/typeorm typeorm sqlite3

そしてTypeORMにDBの情報を教えるため、TypeORMの設定ファイルを用意します。
プロジェクト直下にormconfig.jsonというファイルを作成しましょう。

nest_test/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として確認できるようになります。これはデバックする際の良いヒントとなります。

次にNestJSTypeORMを組み込んでいきます。app.module.tsに追記します。

nest_test/src/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ファイルを生成します。
そして以下のようにコードを書いてください。

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 builddist/entities配下にmemo.entity.jsを出力し、TypeORMのコマンドで、create-memoという名前でマイグレーションファイルを作成しています。

生成されたマイグレーションファイルはこんな感じです。

nest_test/src/migrations/1575868486667-create-memo.ts
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に追記します。

nest_test/src/memo/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リポジトリを生成して渡してくれます。

nest_test/src/memo/memo.service.ts
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サービスを実装していきます。

nest_test/src/memo/memo.service.ts
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コンテナは本当に素晴らしいですね!我々は生成の責務を考えないで実装に専念できます!!

nest_test/src/memo/memo.controller.ts
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/memoGetリクエストすると、空の配列が返ってきます。
localhost:3000/memo?name=fuga&description=hogehogePostリクエストし、再度localhost:3000/memoGetリクエストすると、memoが一つ追加されているのが確認できます。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
image.png
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

結論

これからは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

548
414
3

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
548
414