Open15

Nest.js

high-ghigh-g

Nest.jsとは

  • Node.jsで作成されているバックエンド開発フレームワーク
  • TypeScriptで作られている
  • コアがExpress
  • Angularエクスパイア

Nest.jsのメリット

  • 型の恩恵
  • Expressの機能、ライブラリが利用可
  • NestCLIでプロジェクト、ファイルのテンプレートを生成可
  • テストフレームワークが標準装備
  • 拡張性が高い

Nest.jsのデメリット

  • 既存バックエンドフレームワークと比べると情報が少ない

NestCLI

以下のコマンドを利用可能

nest new プロジェクト名
nest g controller コントローラ名
high-ghigh-g

Nest CLIのプロジェクトについて

  • src
    • spec
      • テストコード
    • controller
    • module
    • service
    • main.ts
      • プロジェクトのエントリーポイント
  • test
    • テストを格納するディレクトリ
high-ghigh-g

Nest.jsの基本アーキテクチャ

コアな要素

  • Controller
  • Service
  • Module

main.ts

app.module.ts

  • ルートモジュール
  • 開発機能を集約し、アプリケーションに登録する役割

    feature.module.ts
    → feature.service.ts
    → feature.controller.ts
high-ghigh-g

Module

  • 関連するController, Serviceをまとめアプリケーションとして利用できるようにする役割
  • Nest.jsには必ず1つ以上のルートモジュールが必要

モジュール定義

classに@Moduleデコレータをつける

@Module({
  ...
})
export class AppModule()

@Moduleデコレーターのプロパティ

  • providers
    • @Injectable デコレーターがついたクラスを記述
  • controllers
    • @Controllerデコレーターがついたクラスを記述
  • imports
    • モジュール内部で必要な外部モジュールを記述
  • exports
    • 外部のモジュールにエクスポートしたいものを記述

サンプルコード

app.module.ts

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService],
  exports: []
})

export class AppModule {}

users.module.ts

@Module({
  imports: [],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService]
})

export class UsersModule {}

Nest.jsのModule利用方法

  • 手動でファイル作成し、一から書いていく
  • Nest CLIを使ってモジュールの雛形を作り、必要部分だけを記載

ターミナルコマンド

nest g module モジュール名

モジュールが作成される

import { Module } from '@nestjs/common';
import { ItemsController } from './items.controller';

@Module({
  controllers: [ItemsController],
})
export class ItemsModule {}

自動でapp.modules.tsにも登録される

import { Module } from '@nestjs/common';
import { ItemsModule } from './items/items.module';

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

high-ghigh-g

Controller

  • ルーティング機能
  • クライアントからリクエストを受け、レスポンスを返す
  • 特定のパスとコントローラが紐づく(/usersとUsersController)

Controllerの定義

  • @Controller()デコレータをつける
  • パスと紐づく

メソッドの定義

メソッドにHTTPメソッドデコレーターをつける

@Controller('users')
export class UsersController {
  @Post()
  create() {
    // Create User
  }
}

コマンド(--no-specをつけるとテストファイルなし)

nest g controller コントローラー名 --no-spec

items.controller.tsが作成される

import { Controller } from '@nestjs/common';

@Controller('items')
export class ItemsController {}
high-ghigh-g

controllerを記述

import { Controller, Get } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get()
  findAll() {
    return 'this is findall';
  }
}

high-ghigh-g

nest.jsの場合、ローカルサーバ立ち上げは npm run start:dev でok
APIの確認は、PostManで確認

上記のコントローラーの場合、以下のurlでok

http://localhost:3000/items
high-ghigh-g

Service

  • ビジネスロジックの定義
  • Controllerから呼び出す

Controllerはルーティング(urlとビジネスロジックの紐づけ)
Serviceはビジネスロジック
として分離する

DI
依存性の注入

Controller → Serviceの依存関係を作る
Controller外部でサービスインスタンスを作成し、Controllerに渡す方法が一般的
外部からインスタンスを渡す作りにすることで、利用するインスタンスを切り替えたいときなどに内部のコードを書き換えずにできる。

  • 本番用とテスト用のインスタンス切り替え
  • ログ出力先の切り替え
    などが容易になる

Nest.jsなどのフレームワークにはDIしやすい仕様が整えられていることが多い

Serviceの定義

import { Injectable } from '@nestjs/common'

@Injectable()
export class UsersService {
  find(userName: number) {
    // findUser
  }
}

作成したサービスをコントローラで使うには

  1. DIの為の設定(モジュールのprovidersに記述)
  2. コントローラ側のconstructorでサービスを引数に取る

NestCLIによるサービスの作成

nest g service items --no-spec

module

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})

export class UsersModule {}

controller

@Controller('users')
export class UsersController {
  constructor(private readonly userService: UsersService) {}

  @Get(':username')
  find(@Param('username') userName: string) {
    this.userService.find(userName)
  }
}
high-ghigh-g

Nest.jsでCRUD実装(create)

@Bodyデコレータを用いることで、リクエストbodyで受け取る値を引数として定義できる

↓コントローラ

  @Post()
  create(
    @Body('id') id: string,
    @Body('name') name: string,
    @Body('price') price: number,
    @Body('description') description: string,
  ): Item {
    const item: Item = {
      id,
      name,
      price,
      description,
      status: ItemStatus.ON_SALE,
    };

    return this.itemsService.create(item);
  }

↓サービス

import { Injectable } from '@nestjs/common';
import { Item } from './item.model';

@Injectable()
export class ItemsService {
  private items: Item[] = [];

  findAll(): Item[] {
    return this.items;
  }

  create(item: Item): Item {
    this.items.push(item);
    return item;
  }
}

update, delete

↓service

  updateStatus(id: string): Item {
    const item = this.findById(id);
    item.status = ItemStatus.SOLD_OUT;
    return item;
  }

  delete(id: string): void {
    this.items = this.items.filter((item) => item.id !== id);
  }

↓controller(Patch, Deleteのそれぞれのデコレータを利用。URLパラメータは@Paramデコレータを利用)

  @Patch(':id')
  updateStatus(@Param('id') id: string): Item {
    return this.itemsService.updateStatus(id);
  }

  @Delete(':id')
  delete(@Param('id') id: string): void {
    return this.itemsService.delete(id);
  }
high-ghigh-g

DTO(Data Transfer Object)

  • データ受け渡しに使われるオブジェクト
    • DBとモデルクラス感のデータのやり取り
    • リクエストオブジェクトからのデータ受けとり

メンテナンス性、安全性が高まる
Nest.jsのバリデーションを利用可能

DTOクラス作成

export class CreateItemDto {
  id: string;
  name: string;
  price: number;
  description: string;
}

controllerのBodyデコレータをDTOで置き換えることができる

  @Post()
  create(
    @Body('id') id: string,
    @Body('name') name: string,
    @Body('price') price: number,
    @Body('description') description: string,
  )

  @Post()
  create(@Body() createItemDto: CreateItemDto)

※Bodyデコレータに渡すパラメータは空でok
※classバリデータを利用するため、DTOがinterfaceやtypeではなくclassで定義する

high-ghigh-g

バリデーション

リクエストオブジェクトの形式チェック

Nest.jsではPipeという機能を利用する

ハンドラーがリクエストを受け取る前にリクエストに対して処理
データの変換、バリデーションが可能
処理を行ったあとのデータをハンドラーに返す
Pipe処理内で例外を返すこともできる

Pipeのメソッド群

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseUUIDPipe
  • DefaultValuePipe

Pipeの利用方法

  • @UsePipesデコレータを利用し、上記のメソッドを定義
  • リクエストパラメータ毎に適用することも可能
  • main.tsでPipeを利用することでグローバルなバリデーションも可能

リクエストパラメータに対して、UUID形式が利用されているかを確認する場合
以下の様に@Paramデコレータの第二引数にParseUUIDPipeを記述

  @Get(':id')
  findById(@Param('id', ParseUUIDPipe) id: string): Item {
    return this.itemsService.findById(id);
  }

Postmanでアクセスすると

localhost:3000/items/test2

↓こういったエラー

{
    "message": "Validation failed (uuid is expected)",
    "error": "Bad Request",
    "statusCode": 400
}

uuid形式だとエラーなしで取得可
localhost:3000/items/3d60e23b-18b8-4cd5-b511-404ac8c2b99e

high-ghigh-g

Class Validatorについて

npm i class-validator class-transformer

DTOに対して以下のような記述を行うことで各プロパティに対するバリデーションを実装することができる

import { Type } from 'class-transformer';
import { IsString, IsNotEmpty, MaxLength, IsInt, Min } from 'class-validator';

export class CreateItemDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(40)
  name: string;

  @IsInt()
  @Min(1)
  @Type(() => Number)
  price: number;

  @IsString()
  @IsNotEmpty()
  description: string;
}

main.tsに以下を記述


import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // 追加

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // 追加
  await app.listen(3000);
}
bootstrap();

Postmanで、バリデーションルールに引っかかる状態でPOSTを実行したとき以下のメッセージが表示される

{
    "message": [
        "name should not be empty"
    ],
    "error": "Bad Request",
    "statusCode": 400
}
high-ghigh-g

NestJSのORMライブラリ

  • TypeORM

※ Prismaもあり

Entity

  • RDBのテーブルと対応するオブジェクト
  • @Entityデコレータをつけたクラスとして定義
  • @PrimaryGeneratedColumnデコレータや@ColumnデコレーターがついたプロパティがRDBのColumnとマッピングされる

Repository

  • Entityを管理する為のオブジェクト
  • EntityとRepositoryが1:1となりDB操作を抽象化する
  • クラスに@EntityRepository()デコレータをつけて、Repositoryを継承する