Lesson 3

DBとの連携

Lesson 3 Chapter 1
初期設定(main.ts)

NestJSの初期設定が行われている「main.ts」を中心に見ていきます。まずNestJSを立ち上げた直後の初期状態としてのディレクトリ構造は以下のようになっています。「main.ts」は「src」配下に格納されています。これから開発を行うときは主に「src」配下のファイルを触ります。

dir_ch4.1.png NestJSのディレクトリ構造

NestJSの基本アーキテクチャ

「main.ts」を見ていく前にNestJSの基本アーキテクチャについて学習しましょう。

NestJSの基本要素

NestJSの基本要素は以下3つの柱で成り立ちます。これらがNestJSの開発のコアとなるもっとも基本的な要素です。原則これらが揃って1つの機能ができ上がります。

  • Controller
  • Service
  • Module

NestJSの基本構成

NestJSは画像のような構成で動きます。その中でも「main.ts」はアプリケーションのエントリーポイント即ちスタート地点と言えます。このファイルを始まりとしてNestJSアプリケーションが動作します。次に「app.module.ts」が存在します。このファイルは「ルートモジュール」と呼ばれます。ルートモジュールは開発した機能を登録して、アプリケーション内で使えるようにする役割を持ちます。反対にルートモジュールへモジュールを登録しなければアプリケーション内で使用できないため注意が必要です。次に「Featureモジュール」が存在します。「Feature」は機能や特徴という意味です。「a.module.ts」や「b.module.ts」等がそれにあたります。これらは実装者が直接実装していくモジュールになります。そのモジュールの下に「service」と「controller」があります。この2つをメインに機能を開発しFeatureモジュールに登録を行います。

以上より基本的な開発の流れとしては以下のようになります。

  1. FeaturesServiceとFeatureControllerを実装
  2. FeatureModuleに開発した機能を登録
  3. ルートモジュールに開発したモジュールを登録

archi_ch4.1.png NestJSの基本構成

main.ts

本題となるmain.tsを見ていきます。nest new ~のコマンドでプロジェクトを作成した人はすでにmain.tsができあがっているかと思います。ファイルの中身を確認してみます。main.ts内ではNestFactory.createというメソッドにルートモジュールを引数として渡すことによってNestJSのインスタンスを生成してくれます。インスタンスを生成することにより自動でHTTPリクエスト待機状態になります。また、await app.listen(3000);でPORT番号の指定ができます。デフォルトは3000で引数を変えることで他のPORT番号を使用できます。

main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

また、main.tsはグローバルなモジュールを扱いたいときにそれを登録する役割を持ちます。たとえば「Pipe」と呼ばれるバリデーションモジュールをグローバルに扱いたいときは以下のように記述します。

注意

※以下は例なので実際に追加する必要はありません。

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

まとめ

main.tsはそこまでたくさん触るファイルではありません。最初は初期設定のまま、必要があれば随時記述を追加します。復習としてmain.tsの役割は下記2点です。

  • アプリケーションのエントリーポイント。アプリケーションのインスタンスを生成してHTTPリクエストの待機状態を作る。
  • グローバルなモジュールや設定を使用する際に追記する。

Lesson 3 Chapter 2
module

moduleは「関連するControllerやServiceなどをまとめ、アプリケーションとして利用できるようにNestJSに登録する」役割を持ちます。図のようにNestJSアプリケーションには必ず1つ以上のルートモジュールと、0個以上のFeatureモジュールが前提となっています。

archi_ch4.1.png NestJSの基本構成

moduleの詳細

DIについて

今後「モジュールを他モジュールへと適用すること」を「DI」と呼称します。DIはDependency Injectionの略です。DIは日本語で「依存性の注入」と呼ばれます。DIは疎結合なモジュールを開発するためのデザインパターンでクラス設計の際にとくに用いられます。DIについて詳しくは本講の最後「Lesson4 Chapter10 Injectableの意味(依存性の注入/他クラスに関数を提供できる)」にて解説していますので気になる方は先にそちらをお読みください。

moduleの詳細を見ていきます。モジュールは@Moduleデコレーターの宣言によりクラスとして定義されます。NestJSでは複数のFeatureモジュールを作成しながらアプリケーションを開発します。「Feature」とつく通り機能ごとにmoduleを切っていくイメージです。極論を言えばNestJSはルートモジュールだけでの開発が可能です。1箇所にロジックをすべて書いて行けばいいからです。しかしそれは望ましいとされていません。NestJSの公式もFeatureモジュールを切って機能ごとに集まりを作り機能群をカプセル化しながら開発していくことを推奨しています。

デコレーター

デコレーターを簡単に説明すると「対象となるクラスや関数に追加効果を付与できる関数」です。@MyFunc()形式で宣言されます。NestJSはデコレーターを用いて開発を行います。最初は記法に慣れないかもしれませんが、書きながら覚えていきましょう。

デコレーターについて

デコレーターはまだTypeScriptの実験的な機能とされており、今後仕様変更の可能性があります。

app.module.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 {}

moduleのプロパティ

moduleで使用されるプロパティを見ていきます。

プロパティ名 説明
providers @Injectableデコレーターが付与されたmodule内でDIの対象となるものを記述
controllers @Controllerデコレーターを付与したコントローラークラスを記述
imports 外部モジュールを記述
exports 外部で扱いたいserviceなどを記述

providers

@Injectableデコレーターが付与されたmodule内でDIの対象となるものを記述します。今後登場しますが、servicerepositoryなどが挙げられます。

controllers

@Controllerデコレーターを付与したコントローラークラスを記述します。Controllerはルーティングの機能を担います。

imports

module内で扱いたい外部モジュールを記述します。たとえばDBと接続したい場合、DBモジュールを登録します。また、Featureモジュールをルートモジュールに登録する際はimportsに記述します。

exports

importsもしくはprovidersに記述した機能のうち外部モジュールでも扱いたいものを記述します。認証機能などモジュールを跨いで横断的に使用したいものはexportsに記述します。

ディレクトリの整理

Todo moduleを作成する前にディレクトリを整理します。まずは不要なファイルを削除します。「app」系のファイルはapp.module.tsのみ使用するためそれ以外を削除します。

~/src
rm app.controller.spec.ts app.controller.ts app.service.ts

また、上記ファイル群を削除したためapp.module.tsの内容を修正します。下記は修正後の内容です。

app.module.ts
import { Module } from '@nestjs/common';

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

次に「modules」ディレクトリを作成し、app.module.tsを格納します。

~/src
mkdir modules
mv app.module.ts modules

また、「app.module.ts」ファイルの場所を移動したため、「main.ts」を以下に修正します。

main.ts
import { NestFactory } from '@nestjs/core';

import { AppModule } from './modules/app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

最終的に「src」の内容が下記画像のようなディレクトリ構造になっていれば準備完了です。

dir_ch4.2-1.png /src内構造

Todo moduleの作成

「Todo module」を作成します。moduleを作成するには2通り方法があります。1つがファイルを自分で作成して1から実装していく方法です。もう1つがNestCLIを利用してコマンドで作成する方法です。本講座は後者を用いて作成します。下記コマンドでTodo moduleを作成しましょう。

~/nest-todo
nest g module modules/todo

ディレクトリ構造について

作成するTODOアプリのディレクトリ構造については「Lesson1 Chapter5本講における実装方針」にて解説しています。

「g」は「generate」の略です。このコマンドだけでmoduleが作成できます。「src」配下を見て見ましょう。画像のように「modules/todo/todo.module.ts」ができていれば成功です。

dir_ch4.2-2.png

Todo moduleの内容

作成したmoduleはプロパティがまだ存在しません。こちらは後でControllerServiceを作成した際に登録します。

todo.module.ts
import { Module } from '@nestjs/common';

@Module({})
export class TodoModule {}

Todo moduleの登録

moduleを作成したのでルートモジュールに登録する必要があります。しかし、「app.module.ts」の内容を見るとすでに登録されているのが確認できます。NestCLIを使うことで作成したmoduleの登録まで行なってくれます。

app.module.ts
import { Module } from '@nestjs/common';
import { TodoModule } from './todo/todo.module';

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

まとめ

これでmoduleについては以上になります。moduleは機能を作成するときの基本単位となることを意識しながら開発を行なっていきましょう。

Lesson 3 Chapter 3
entity

このChapterではTodo Entityの作成を通してEntityについて学びます。

Entity(エンティティ)

プログラミングの世界で、エンティティとは対象とする「実体」に対する「抽象概念」として定義づけられます。「実体」とはRDBの「テーブル」そのものです。プログラミングの世界でエンティティはRDBの文脈で用いられることがほとんどです。そしてプラグラミングの世界で「抽象化」といえば「型」や「クラス」のことです。つまりエンティティは「テーブルの型やクラス」のことを指します。

エンティティの役割は以下2点です。

  • 「実体」を生成する
  • 「参照」に使用する
「実体」を生成する

エンティティは「実体」である「テーブル」を作成するときの元情報となります。テーブル名とカラム名(属性名)が定義されたクラスを元にテーブル(実体)が生成されます。

「参照」に使用する

プログラムからテーブル情報を参照したい時エンティティとして定義されているクラスを参照します。クラスはあくまで「抽象概念」なので実際にテーブルにアクセスするための具体的なプログラムは裏で動きます。

Entity with NestJS

NestJSではEntityクラスを定義することでRDBのテーブル定義と同等のことを行えます。「抽象概念」として定義したtodo.entity.tsをもとにテーブルが作成されます。また、todo.entity.tsはJavaScriptのオブジェクトとRDBのデータ構造をマッピングしてくれるクラスとしても働きます。マッピングしてくれることでJavaScriptのオブジェクトを扱うようにRDBを参照・操作できるようになります。

entity_ch4.3.png NestJSとエンティティ

Todo Entityの作成

Todo Entityを作成しRDBのテーブル定義を行なってみましょう。「src」ディレクトリの配下に「entities」ディレクトリを作成します。「entities」ディレクトリ配下に「todo.entity.ts」ファイルを作成します。コマンドも載せておきます。

~/nest-todo/src/
mkdir entities && cd entities
touch todo.entity.ts

Todo Entity

「todo.entity.ts」ファイルでTodo Entityを定義します。まずは@Entityデコレーターを付与したTodoクラスを定義します。クラス名がテーブル名に反映されます。

todo.entity.ts
import { Entity } from 'typeorm';

@Entity()
export class Todo {}

カラムの定義

次にTodoテーブルのカラムとなるプロパティを定義します。カラムは「型」と「デコレーター」によって成り立ちます。

todo.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Todo {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 128, nullable: false })
  title: string;

  @Column({ default: false, nullable: false })
  isCompleted: boolean;

  @Column({ nullable: false })
  createdAt: string;

  @Column({ nullable: false })
  updatedAt: string;
}

@PrimaryGeneratedColumn('uuid')

主キーとして定義するカラムに付与するデコレーターです。引数に何も付与しない場合number型の自動採番になります。今回はUUID型としています。

自動採番

「1, 2, 3...」と前の数字にしたがって自動で増加していく数字のことです。

uuid

uuidとは全世界でいっさい被らないとされている一意なIDのことです。

@Column()

このデコレーターを付与することでEntityにカラムであることを認識させることができます。主キー以外には必須となります。

Entity(テーブル)の内容

カラム名 説明
id Todoごとの一意なID・識別子。参照や削除の際に使用する。
title Todoのタイトル。最高128文字。
isCompleted Todoの完了フラグ。デフォルトはfalse
createAt Todoの作成日。
updatedAt Todoの更新日。

エンティティのbooleanとテーブルのtinyint(1)

RDBのテーブルでは真偽値は「0/1」で表されます。これをMySQLではtinyint(1)型と呼びます。0がfalseで1がtrueです。

まとめ

Entityの作成まで行うことができました。今後このEntityを使用しながら開発を行なっていきます。また本格的にRDBを使用する際にはEntityをもとにテーブルを作成することを行います。

Lesson 3 Chapter 4
controller

このChapterではTodo Controllerの作成を通してcontrollerについて学びます。

controller

controllerは「Lesson1 Chapter4 MVCモデル」でModelViewに指示を出したりデータを渡すための司令塔であり、「ルーティング」を主に担当すると説明しました。また、3層アーキテクチャのプレゼンテーション層としてクライアントからの情報をビジネス層(Service)に渡します。ルーティングは、「どのコントローラーがどのリクエストを受信するか」を制御します。多くの場合、各コントローラーごとに異なるパスを担当し異なるアクションを実行できます。

controller_ch4.4.png

Todo Controller

早速Todo Controllerを作成します。以下コマンドを実行しましょう。自動で「controllers/todo/todo.controller.ts」ができあがります。--no-specオプションでテストファイル作成を回避できます。現時点では必要ないのでオプション付与で作成しましょう。

~/src
nest g controller controllers/todo --no-spec

moduleへの登録

controllerを作成したらmoduleに登録する必要があります。「todo.module.ts」に下記のように登録します。

todo.module.ts
import { Module } from '@nestjs/common';
import { TodoController } from 'src/controllers/todo/todo.controller';

@Module({
  // 追加
  controllers: [TodoController],
})
export class TodoModule {}

Controller 初期状態

Todo Controllerの初期ファイルを見てみます。

todo.controller.ts
import { Controller } from '@nestjs/common';

@Controller('todo')
export class TodoController {}

NestJSからControllerをimportします。@Controllerというデコレーターを付与することでコントローラークラスを定義できます。@Controllerは任意で引数を取ることができます。引数はパスを表します。Todo Controllerは「/todo」でリクエストパスがグループ化されます。

レスポンスを返す

「/todo」へのGETリクエストに対して「Hello Todo!」という文字列を返すコードを書いてみます。書き上げたらnpm run start:devでサーバーを起動し「http://localhost:3000/todo/」へアクセスしましょう。「Hello Todo!」とレスポンスが返れば成功です。

todo.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('todo')
export class TodoController {
  @Get()
  hello() {
    return 'Hello Todo!';
  }
}

constructor

次章でServiceを作成するため下記コードはあくまで例になります。実際のコードに記述する必要はありません。

constructor
constructor(private readonly appService: AppService) {}

constructorへはDI(依存性の注入)を行いたいService等を記述します。ServiceインスタンスではなくServiceクラスを渡していることがポイントです。DIにより疎結合な関係を維持しています。

DIについて

今後「モジュールを他モジュールへと適用すること」を「DI」と呼称します。DIはDependency Injectionの略です。DIは日本語で「依存性の注入」と呼ばれます。DIは疎結合なモジュールを開発するためのデザインパターンでクラス設計の際にとくに用いられます。DIについて詳しくは本講の最後「Lesson4 Chapter10 Injectableの意味(依存性の注入/他クラスに関数を提供できる)」にて解説していますので気になる方は先にそちらをお読みください。

@Get()

@Get()
@Get()
  hello() {
    return 'Hello Todo!';
  }

@Getデコレーターを使用することでコントローラー内のメソッドをGETリクエストに対応させることができます。このときのルートパスは@Controllerの引数に指定した値です。本コントローラーの場合「http://localhost:3000/todo」へGETリクエストが来た場合helloを実行します。

また、@Getの引数により@Controller('todo')の「/todo」配下でパスを自由に生成できます。「http://localhost:3000/todo/hello」にリクエストを対応させたい場合下記になります。

http://localhost:3000/todo/hello
@Get('hello')
  hello() {
    return 'Hello Todo!';
  }

動的なパラメーター

「/todo」配下で動的なパラメーターを作成できます。たとえば取得したいTODOを絞りたい時に使えます。以下は「http://localhost:3000/todo/hoge」や「http://localhost:3000/todo/foo」といった「/todo」配下を動的なパラメーターに対応させることができます。「http://localhost:3000/todo/hoge」とリクエストを送るとレスポンスが確認できます。

todo.controller.ts
...省略
@Get(':id')
  hello() {
    return 'Hello Todo!';
  }
...省略

まとめ

以上がcontrollerの基本的な動きになります。今後もservice等を導入したり、アプリを作成しながらcontrollerの仕組みを学んでいきましょう。

Lesson 3 Chapter 5
service

このChapterではTodo Serviceの作成を通してserviceについて学びます。

service

serviceは「Lesson1 Chapter4 MVCモデル」でModelに該当しビジネスロジックを担当すると説明しました。ビジネスロジックとはアプリケーション独自の要件を実装するコアロジックのことです。また、3層アーキテクチャのビジネス層としてプレゼンテーション層のcontrollerから受け取った情報を元にビジネスロジックを構築したり、データアクセス層のrepositoryを操作してDBからの情報を処理します。

service_ch4.5-1.png controller, service, repositoryの関係図

Todo Service

早速Todo Serviceを作成します。以下コマンドを実行しましょう。自動で「services/todo/todo.service.ts」ができあがります。

~/src
nest g service services/todo --no-spec

moduleへの登録

serviceを作成したらmoduleに登録する必要があります。「todo.module.ts」に下記のように登録します。providersへはrepository等も後で登録します。

todo.module.ts
import { Module } from '@nestjs/common';
import { TodoController } from 'src/controllers/todo/todo.controller';
import { TodoService } from 'src/services/todo/todo.service';

@Module({
  controllers: [TodoController],
  // 追加
  providers: [TodoService],
})
export class TodoModule {}

Service 初期状態

Todo Serviceの初期ファイルを見てみます。

todo.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {}

NestJSからInjectableをimportします。@Injectableというデコレーターを付与することで他クラスにDI可能なクラスとして定義づけできます。serviceはControllerから参照します。そのためControllerにDI可能なように@Injectableデコレーターを付与します。@Service()デコレータ等は使用せず、serviceであることをファイル名から判断します。

@Service()ではないことに注意

controllerへの登録

servicecontrollerから使用ためには2つのステップがあります。

  1. moduleへ登録
  2. controllerへ登録(DI
1. module(todo.module.ts)に登録することでcontrollerへのDIの仕組みを利用できるようになります。2. controllerconstructorの引数へ代入しDIします。1は「moduleへの登録」で実装済みであるため、2を下記コードで記述します。

serviceとDI

serviceを扱う上でDIはとても重要な概念であるためこの講座の最後に詳しく解説します。DIの説明の前に一度処理の流れを実装してその流れを元に解説を行います。

todo.controller.ts
import { Controller, Get } from '@nestjs/common';
import { TodoService } from 'src/services/todo/todo.service';

@Controller('todo')
export class TodoController {
  // 追加
  constructor(private readonly todoService: TodoService) {}
  ...省略
}

private修飾子

private修飾子を付与したプロパティはクラスの中でのみ利用できるプロパティとして登録できます。

readonly修飾子

readonly修飾子を付与したプロパティは中身を変更することはできず参照のみとなります。

serviceのビジネスロジック

serviceにビジネスロジックを記述します。

todo.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class TodoService {
  hello(): string {
    return 'Hello todo with service!';
  }
}

serviceのビジネスロジックをcontrollerから利用

次にserviceのビジネスロジックをcontrollerから利用します。今回はcontrollerに元々書いていた処理をserviceに移しただけになります。この状態で「http://localhost:3000/todo/」へGETリクエストを飛ばすと「Hello todo with service!」という文字列が返ります。

todo.controller.ts
import { Controller, Get } from '@nestjs/common';
import { TodoService } from 'src/services/todo/todo.service';

@Controller('todo')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}
  @Get()
  // 変更
  hello(): string {
    return this.todoService.hello();
  }
}

serviceとcontrollerそれぞれの責務

今回のビジネスロジックは呼ばれると文字列を返すだけの簡単なものでした。かつcontrollerから処理をそっくりそのまま移しただけでした。このことからserviceに書くロジックはcontrollerへ直接書くことができます。しかし、controllerの責務は「ルーティングを行うこと」でした。責務を無視してcontrollerにビジネスロジックを書いてしまうとcontrollerの中身がファットになりいずれコードは崩壊してしまいます。崩壊を逃れるために3層アーキテクチャのように層に分けてアプリケーションを考える設計が存在します。「保守性・拡張性」に優れたコードへするために層の責務はしっかり意識しながらコーディングしていきましょう。

Dependency Injection(依存性の注入)

度々登場しているDI(Dependency Injection)について解説します。DIは日本語で「依存性の注入」と訳されます。DIの目的は「疎結合なモジュール」を実現することです。「疎結合」とは「変更が依存先に影響しないこと」を指します。今回の実装を元に解説します。

controllerからserviceを利用

controllerからserviceを利用するには、constructorの引数としてserviceを代入していました。TodoServiceは型として利用しています。それをメソッドの中で参照しています。

todo.controller.ts
constructor(private readonly todoService: TodoService) {}
  @Get()
  hello(): string {
    // 利用
    return this.todoService.hello();
  }

これは下記のようにしても同じ動きが再現できます。「http://localhost:3000/todo/」へGETリクエストを飛ばすと「Hello todo with service!」の返却が確認できます。

todo.controller.ts
@Controller('todo')
export class TodoController {
  todoService: TodoService;
  constructor() {
    this.todoService = new TodoService();
  }

  @Get()
  hello(): string {
    return this.todoService.hello();
  }
}

controllerとserviceが密結合な状態

「インスタンス化するTodoServiceを変更したい」という要望があるとどうなるでしょうか?たとえば本番用のTodoServiceとテスト用のTodoServiceが別れている場合、環境に合わせてcontrollerの中身を変更する必要が出てきます。環境に合わせてクラスを変更する理由としては本番環境独自のロジック(課金等)が混ざっているパターンがあるからです。

todo.controller.ts(テスト用)
constructor() {
    this.todoService = new TodoServiceForTest();
}
todo.controller.ts(本番用)
constructor() {
  this.todoService = new TodoServiceForProduction();
}

controller内で環境に合わせたif文を書いてインスタンス化する対象を変えればいいかもしれません。しかしそれではcontrollerの関心事が増えてしまいます。controllerは「ルーティング」に集中させるべきです。環境の違いを知る必要はありません。controllerやserviceはアプリケーションを支えるコアな機能です。であればアプリケーションを動かすための本質的な処理に集中させるべきです。境の違いはmoduleやそれを統括する「main.ts」等が気にするべきことです。

このように要件の変更等によって「依存している対象を変更する必要が出てくる」ような状態を「密結合」といいます。今回の場合はcontrollerとserviceが依存しているため「controllerとserviceは密結合な状態である」と言えます。

Dependency Injection(DI)による疎結合化

この問題を解決するためのデザインパターンがDependency Injection(DI)です。DIを用いると依存先を疎結合に注入できます。

controllerのconstructorに着目します。constructorは引数でTodoServiceのインスタンスを受け取るようになっています。つまり型が同じであれば異なるインスタンスを受け取ることができる状態にあります。インスタンスがテスト用か本番用か気にする必要はありません。このようにインスタンスを引数で渡して疎結合な状態を作ることをDependency Injectionと呼びます。

todo.controller.ts
constructor(private readonly todoService: TodoService) {}

DIコンテナ

疑問に思うことが1つあります。controllerとserviceのインスタンスはどこで作られているのでしょうか?どこにもnewしているような記述がありません。通常下記のようにしてインスタンスを生成するかと思います。controllerをインスタンス化してそこにインスタンス化したserviceを渡します。しかしそのコードはどこにも見当たりません。ではなぜDI機構が使用できているのでしょうか?

const todoController = new TodoController(new TodoService())

答えは「その詳細をNestJSがラップしてくれているから気にする必要はない」です。「todo.module.ts」を見てみます。インスタンス化はしていませんがcontrollerやserviceを登録しています。moduleに登録することでNestJSが必要に応じてDIしてくれます。これはDIを採用しているフレームワークによくみられる手法で「DIコンテナ」と呼ばれます。NestJSのフレームワークの仕組みの恩恵により私たちはDIをとても簡単に実現することができています。

todo.module.ts
@Module({
  controllers: [TodoController],
  providers: [TodoService],
})

まとめ

このChapterでは「serviceとは」、「controllerでの使い方」、「Dependency Injection(依存性の注入)」(DI)について説明しました。serviceの処理の詳細は簡単なものでしたがDBとの連携を行う際にビジネスロジックをたくさん書いていきます。また、DIは今後頻繁に登場する用語になります。次はデータアクセス層である「repository」について解説します。

Lesson 3 Chapter 6
repository

このChapterではTodo Repositoryの作成を通してrepositoryについて学びます。

repository

repositoryはserviceとDBとの仲介役を行うクラスのことです。DBを参照し、データをserviceに返します。repositoryはEntityを元に生成されます。Entityの情報をもとにDBを操作するためです。repositoryを作成する際はEntityがかならず必要です。repositoryは3層アーキテクチャの中で「データアクセス層」を担当します。

repositoryの定義

repositoryを作成します。もっとも簡単なのはserviceにそのままDIすることです。他にはrepository用のファイルを作成して定義したカスタムrepositoryクラスをserviceにDIすることもできます。まず、前者をコードで見てから後者を実装します。

repositoryの定義 serviceに直接編

まずはrepository用のファイルを作成せずserviceで定義してみます。InjectRepository@nestjs/typeormからimportします。そしてconstructorの中でInjectRepositoryを用いてrepositoryをDIします。

todo.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from 'src/entities/todo.entity';
import { Repository } from 'typeorm';

@Injectable()
export class TodoService {
  constructor(
    @InjectRepository(Todo) private todoRepository: Repository<Todo>,
  ) {}
  hello(): string {
    return 'Hello todo with service!';
  }
}
...省略

次にrepositoryを使用するための新しいメソッドを定義します。Todoの一覧を取得するメソッドを追加します。serviceの中ではDIしたtodoRepositorythisで参照しながら開発します。

todo.service.ts
...省略
@Injectable()
export class TodoService {
  constructor(
    @InjectRepository(Todo) private todoRepository: Repository<Todo>,
  ) {}
  hello(): string {
    return 'Hello todo with service!';
  }

  getAllTodos() {
    return this.todoRepository.find();
  }
}
...省略

ここでgetAllTodosにホバーしてみましょう。すると返り値の型が表示されます。repositoryはDB操作をラップしてくれているためあらかじめ複数のメソッドが定義されています。findはすべての一覧を取得するメソッドです。

type_ch4.6.png findの返り値型

他のメソッドが気になる方はRepository<Todo>のRepositoryにカーソルを当ててF12を押すことで定義されているメソッドの型を見ることができます。Entityは今回だとTodoに該当します。

Repository.d.ts(findメソッドの型定義の例)
/**
  * Finds entities that match given find options.
  */
find(options?: FindManyOptions <Entity>): Promise<Entity[]>;

返り値の方は明示的に書いたほうがわかりやすいため追記しておきましょう。またこの場合Promiseを返すためasync/awaitを付与して同期的に扱いやすいようにします。

todo.service.ts
...省略
@Injectable()
export class TodoService {
  ...省略

  async getAllTodos(): Promise<Todo[]> {
    return await this.todoRepository.find();
  }
}
...省略

custom repository

なぜカスタムrepositoryを作成するのか

目的はメソッドのオーバーライドです。オーバーライドとは上書きのことです。アプリケーションを作成する中で元あるメソッドの処理に手を加えたい時が出てきます。そのためにカスタムrepositoryを作成します。

カスタムrepositoryは元のRepositoryクラスを継承するため元のRepositoryと同様の振る舞いが可能です。そのため定義したからといって無理にメソッドを定義する必要はありません。必要なものだけオーバーライドします。また、DBとの連携をまだ行なっていないため今回はカスタムrepositoryを定義するだけになります。メソッドのオーバーライドはDBとの連携を行う際改めて説明します。

カスタムrepositoryファイルの作成

カスタムrepositoryは別ファイルに切り分けることで定義できます。下記コマンドで作成します。

~/src
mkdir repositories && cd repositories
touch todo.repository.ts

todo.repository.ts

作成したカスタムrepositoryを編集します。

  • @Injectable()デコレーターを付与してDI可能クラスを生成します。
  • TodoRepositoryをクラス宣言しRepositoryクラスを継承しRepositoryのプロパティへアクセス可能にします。
  • constructorで元のRepositoryクラスをDIし、このカスタムrepositoryクラスをrepositoryとして振る舞うようにします。
  • constructorsuperを呼び出して継承元のRepositoryクラスのconstructor関数を実行します。
todo.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from 'src/entities/todo.entity';
import { Repository } from 'typeorm';

@Injectable()
export class TodoRepository extends Repository<Todo> {
  constructor(@InjectRepository(Todo) repository: Repository<Todo>) {
    super(repository.target, repository.manager, repository.queryRunner);
  }
}

todo.module.ts

作成したカスタムrepositoryを他クラスにDIできるようにprovidersへ登録します。

todo.module.ts
import { Module } from '@nestjs/common';

import { TodoController } from 'src/controllers/todo/todo.controller';
// TodoRepositoryインポート
import { TodoRepository } from 'src/repositories/todo.repository';
import { TodoService } from 'src/services/todo/todo.service';

@Module({
  controllers: [TodoController],
  // TodoRepository追加
  providers: [TodoService, TodoRepository],
})
export class TodoModule {}

todo.service.ts

実際にserviceでDIします。あとは通常のrepositoryのようにTodoRepositoryを扱います。

todo.service.ts
import { Injectable } from '@nestjs/common';
import { TodoRepository } from 'src/repositories/todo.repository';

@Injectable()
export class TodoService {
  // カスタムrepositoryをDI
  constructor(private todoRepository: TodoRepository) {}
  hello(): string {
    return 'Hello todo with service!';
  }

  async getAllTodos() {
    return await this.todoRepository.find();
  }
}

repository使用時の注意点

repositoryはDBとの連携を行うために使用されます。現在はDBとの連携処理を書いていないため現状のソースコードでサーバーを立ち上げるとエラーとなってしまいます。DBとの連携は「Lesson3 Chapter12 MySQLとの接続」で行うため現場追加したコードはコメントアウトしておきましょう。

todo.module.ts
import { Module } from '@nestjs/common';
// コメントアウト
// import { TypeOrmModule } from '@nestjs/typeorm';

import { TodoController } from 'src/controllers/todo/todo.controller';
// コメントアウト
// import { Todo } from 'src/entities/todo.entity';
// コメントアウト
// import { TodoRepository } from 'src/repositories/todo.repository';
import { TodoService } from 'src/services/todo/todo.service';

@Module({
  imports: [],
  controllers: [TodoController],
  // コメントアウト
  // providers: [TodoService, TodoRepository],
  providers: [TodoService],
})
export class TodoModule {}
todo.service.ts
import { Injectable } from '@nestjs/common';
// コメントアウト
// import { TodoRepository } from 'src/repositories/todo.repository';

@Injectable()
export class TodoService {
  // コメントアウト
  // constructor(private todoRepository: TodoRepository) {}
  hello(): string {
    return 'Hello todo with service!';
  }

  // コメントアウト
  // async getAllTodos() {
  //   return await this.todoRepository.find();
  // }
}

まとめ

今回はカスタムrepositoryの定義まで行いました。今後要件に応じて元のRepositoryのメソッドをオーバーライドするタイミングが出てきます。DBとの連携を行うときにまた詳しく説明します。

Lesson 3 Chapter 7
request

このChapterではクライアントから渡されるリクエストパラメーターやリクエストBodyをNestJSではどう扱うかについて学んでいきます。Chapterの流れとしては下記になります。

  1. リクエストパラメーター
  2. リクエストBody
  3. DTOを使ったリクエストBody

DTOについて

DTOについては扱う箇所で詳細に解説します。

リクエストパラメーター

クライアントからパスやクエリなどどのように受け取るか学びます。

クエリパラメーター

クエリパラメーターは@Query()デコレーターを付与することで取得できます。以下のように書いてみてnpm run start:devでサーバーを立ち上げたら「http://localhost:3000/todo?page=10」でリクエストを飛ばしてみましょう。コンソールに「10」と表示されます。

todo.controller.ts
...省略
@Controller('todo')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}
  @Get()
  findAll(@Query('page') page?: string) {
    console.log(page);
  }
}

次にオブジェクトでも取得してみましょう。下記のようにコードを変更してください。{ page: 10 }がコンソールに表示されていることが確認できます。

todo.controller.ts
@Get()
findAll(@Query() query: { page?: string }) {
  console.log(query);
}

また、複数のプロパティでも取得できます。下記のように変更してみましょう。「http://localhost:3000/todo?page=10&id=aaa」でリクエストを送ると{ page: 10, id: 'aaa' }がコンソールに表示されていることが確認できます。

todo.controller.ts
@Get()
findAll(@Query() query: { page?: string, id?: string }) {
  console.log(query);
}

クエリパラメーターはオプショナルパラメーター

クエリパラメーターはオプショナルなパラメーターなため{ page?: string }のような型定義を行います。オプショナルとは「任意の」という意味です。オブジェクトにプロパティが含まれていなくても問題ありません。

パスパラメーター

パスパラメーターは@Param()デコレーターを付与することで取得できます。新しくTodo詳細取得用のfindByIdメソッドを定義します。「http://localhost:3000/todo/aaa」でリクエストを送ると「aaa」という文字列がターミナルに表示されます。

todo.controller.ts
@Get(':id')
findById(@Param('id') id: string) {
  console.log(id);
}

クエリと同じようにオブジェクトでも取得します。

todo.controller.ts
@Get(':id')
findById(@Param() param: { id: string }) {
  console.log(id);
}

パスパラメーターは必須パラメーター

クエリパラメーターがオプショナルであることに対してパスパラメーターは必須パラメーターです。データを取得するために必須のパラメーターをパスに含めます。

リクエストBody

リクエストBodyは@Body()デコレーターを付与することで取得できます。新しくPOSTでcreateメソッドを作成します。

todo.controller.ts
@Post()
create(@Body('title') title: string) {
  console.log(title);
}

Thunder ClientのBody/Form-encodeに「title」というフィールドを追加して好きな値を追加し「http://localhost:3000/todo/」へリクエストを送りましょう。「title」を含んだオブジェクトが取得できます。

thunder_ch4.7.png Thunder Client POST リクエスト

複数指定したいときは下記のようにできます。

todo.controller.ts
@Post()
create(
  @Body('title') title: string,
  @Body('isCompleted') isCompleted: boolean,
) {
  console.log(title);
  console.log(isCompleted);
}

オブジェクトの指定も可能です。

todo.controller.ts
@Post()
create(@Body() body: { title: string; isCompleted: boolean }) {
  console.log(body);
}

DTOを用いたリクエストBodyのリファクタリング

ここからリクエストBodyにDTOを適用します。

DTO

DTO(Data Transfer Object)はソフトウェア開発において「データの受け渡しのために使われるオブジェクト」のことを言います。「Transfer」は日本語で「転送・転移」を意味する英単語です。ソフトウェア開発において「データの受け渡し」は「リクエストとレスポンス」にて行われます。

DTOの構造

DTOはクラスで定義します。デコレーターでバリデーションの機能を付与したいからです。DTOは「型定義+バリデーション」の構造で成り立ちます。このChapterでは型定義のみを行います。

バリデーション

バリデーションとは「形式チェック」のことです。たとえばメールアドレスは「メールアドレスの形式(@が含まれているか等)」であるか、パスワードは英数字8文字以内かなど入力値の最低限の形式を満たしているかチェックをすることをバリデーションと呼びます。

DTOを定義する

TODO作成時のリクエストDTOを作成します。今回はバリデーションがついていない純粋な型定義のみのDTOを定義します。「src」配下に「requests」というディレクトリを作りそこに格納します。また、TODO用のDTOを定義するため「requests」配下に「todo」ディレクトリを切ってそこでファイルを管理します。

~/src
mkdir requests && cd requests
mkdir todo && cd todo
touch create.dto.ts

DTOを定義します。TODOのタイトルは必須ですが完了未完了は任意としてます。完了未完了がない場合は未完了(false)で定義するようにします。

create.dto.ts
export class CreateTodoRequestDTO {
  title: string;
  isCompleted?: boolean;
}

DTOルートファイルの作成

DTOを一括でエクスポートする用のルートファイル作成します。ルートファイルのメリットとしてはインポート元がひとつになる、インポート元がひとつになることでインポート箇所の記述がシンプルになる、格DTOファイル名の変更がルートファイルのみで済む等です。デメリットはルートファイルでエクスポートするファイルが互いに参照し合わないように気をつけないといけない点です。循環参照でエラーとなってしまうためです。実際に使用して確かめてみましょう。

~/src/requests/todo
touch index.ts
~/src/requests/todo/index.ts
export * from './create.dto';

DTOの適用

ControllerにDTOを適用しServiceに接続します。「todo.service.ts」にcreateメソッドは存在しないため型エラーが出ますが次で作成するためそのままで大丈夫です。

todo.controller.ts
...省略
import { CreateTodoRequestDTO } from 'src/requests/todo';
...省略
@Post()
create(@Body() createDto: CreateTodoRequestDTO) {
  this.todoService.create(createDto);
}

Serviceでもcreateメソッドを定義してDTOを受け取ります。今回はconsoleに表示しましょう。この状態で「http://localhost:3000/todo」へPOSTリクエストを送りましょう。送った内容がconsoleに表示されればOKです。

todo.service.ts
...省略
import { CreateTodoRequestDTO } from 'src/requests/todo';
...省略
@Injectable()
export class TodoService {
  // constructor(private todoRepository: TodoRepository) {}
  hello(): string {
    return 'Hello todo with service!';
  }

  // async getAllTodos() {
  //   return await this.todoRepository.find();
  // }

  // 追加
  create(createDto: CreateTodoRequestDTO) {
    console.log(createDto);
  }
}

まとめ

今回はDTOについて解説しました。DTOにはバリデーションも含まれるのですがそれは「Lesson4 Chapter4 バリデーションの実装」にて解説しています。次はResponseにもDTOを定義します。

Lesson 3 Chapter 8
response

responseにもDTOを定義していきましょう。

レスポンスDTOを作成する

ファイルの作成

TODO作成時のレスポンスDTOを作成します。リクエストと同じくバリデーションがついていない純粋な型定義のみのDTOを定義します。前回同様ディレクトリとファイルを作成します。

~/src/
mkdir response && cd response
mkdir todo && cd todo
touch create.dto.ts
touch index.ts

DTOの定義

TODOを作成したときのレスポンスは作成したTODOをそのまま返すようにします。

~/src/response/create.dto.ts
export class CreateTodoResponseDTO {
  id: string;
  title: string;
  isCompleted: boolean;
  createdAt: string;
  updatedAt: string;
}

ルートファイルにも記述を追加します。

~/src/response/index.ts
export * from './create.dto';

レスポンスDTOの適用

serviceとcontrollerにそれぞれDTOを適用します。

serviceへの適用

のちのDBとの接続時にはPromiseを返しますが、現在はシンプルな形を保ちたいので下記のように定義しておきましょう。

~/src/services/todo/todo.service.ts
...省略
import {
  CreateTodoResponseDTO,
  FindAllTodoResponseDTO,
  FindOneTodoResponseDTO,
} from 'src/response/todo';
...省略

create(createTodoDto: CreateTodoRequestDTO): CreateTodoResponseDTO {
    return {
      id: 'test id',
      title: createTodoDto.title,
      isCompleted: createTodoDto.isCompleted,
      createdAt: 'test createdAt',
      updatedAt: 'test updatedAt',
    };
  }

controllerへの適用

controllerへも同様に適用します。レスポンス型を返すようにしたらserviceを返すようにしましょう。

~/src/services/todo/todo.controller.ts
@Post()
  create(@Body() createDto: CreateTodoRequestDTO): CreateTodoResponseDTO {
    return this.todoService.create(createDto);
  }

まとめ

レスポンスDTOの適用は以上になります。今後DBとの接続でserviceやcontrollerが増えていきますのでそれ必要に応じてDTOを増やします。

Lesson 3 Chapter 9
MySQLとの接続

このChapterではDB(MySQL)とNestJSを接続します。DBとの接続完了をこのChapterではゴールとします。DBとの接続は設定項目が多く大変ですが頑張りましょう。

パッケージのインストール

環境変数周りの設定に必要なパッケージをインストールします。

~/nest-todo
npm i @nestjs/config

コンフィグの設定

「ConfigModuleの設定」では環境変数を設定し、ConfigModuleの設定を完了させることを目標とします。

環境変数

アプリケーション(以下App)の実行は開発、本番など異なる環境で行われます。その際DBへの接続情報は異なることが多いです。DBへの接続情報はサーバー内へ変数として配置することでソースコード等から外部に公開できない仕組みとし、それをAppから参照するという方法をよく取られます。それを実現する仕組みが環境変数です。NestJSでは.envファイルをプロジェクト配下に配置することで環境変数をサーバーに公開・利用できます。

環境変数の設定

環境変数設定の用のファイルを作成します。今回は開発用環境変数になるため名前を「.development.env」としています。

~/test-todo
touch .development.env

git利用時の注意点

gitを利用している場合、そのままだとenvファイルがGithub等に公開されてしまいます。公開されないように「.gitignore」にenvファイルを含める必要があります。今回は開発用のenvファイルなので公開しても構いませんが本番用のenvファイルが公開されないように*.envのように記述を追加しましょう。

DBへの接続情報を「.development.env」へ追加します。

.development.env
// ローカルでDBを立ち上げている人はlocalhost, docker等使用している方はdocker-composeファイル等で指定したホスト名
DB_HOST=localhost
// 他と被らないport番号
DB_PORT=3310
// DBログインユーザー
DB_USER=root
// DBログイン用パスワード
DB_PASSWORD=password
// 今回使用するDB名(任意)
DB_NAME=nesttodo

configurationファイルの作成

初めにインストールした@nestjs/configは環境変数をNestJSで扱いやすくするパッケージです。CoNfigModuleをパッケージからインポートしてDIする必要があります。また、今回カスタムconfigファイルを作成しそこから環境変数の値を取得します。

~/src
mkdir config && cd config
touch configuration.ts

カスタムconfigファイルは環境変数を意味のあるまとまりとして管理するため使用します。下記のように記述することで通常configService.get('DB_HOST')とアクセスするところをconfigService.get('db.host')のようにアクセスできるようになります。今回は環境変数の種類が少ないためメリットを感じにくいかもしれませんが、このように管理することでコードを見る人に意図を伝えやすく実装上のミスも起きにくくなります。

~/src/config/configuration.ts
export default () => ({
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 3306,
    user: process.env.DB_USER || 'root',
    password: process.env.DB_PASSWORD || 'password',
    name: process.env.DB_NAME,
  },
});

ConfigModule 設定

ConfigModuleを使う準備が整ったので「app.module.ts」にて設定を行います。ConfigModule設定の最小構成は下記になります。forRootメソッドの引数に設定を追加します。

~/src/modules/app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})

envFilePathプロパティで環境変数格納ファイルを指定し、 loadプロパティに先ほど作成した「configuration.ts」を指定してカスタムコンフィグファイル経由で環境変数アクセスを可能にします。

~/src/modules/app.module.ts
...省略
import { ConfigModule } from '@nestjs/config'; // 追加
import configuration from 'src/config/configuration'; // 追加
...省略
@Module({
  imports: [
    TodoModule,
    // 追加
    ConfigModule.forRoot({
      envFilePath: '.development.env',
      load: [configuration],
    }),
  ]
})

DB接続設定(TypeORMModule)

DB接続のための準備は整ったのでTypeORMModuleの設定を行いDBへ接続します。下記は現在の「app.module.ts」の内容です。

TypeOrmModule.forRootAsyncメソッドの引数にDBへの接続情報を渡します。また、TypeORMModuleの中でConfigModuleを使用するためimports: [ConfigModule]の指定をし、ConfigServiceをinject: [ConfigService]でDIしています。

プロパティ名 説明
type DBのタイプを指定。今回はMySQLを使用。
PostgreSQLを使用するときはtype: 'postgres'
host 環境変数にて設定したDBのhost
port 環境変数にて設定したDBのport
username 環境変数にて設定したDBのusername
password 環境変数にて設定したDBのpassword
database 環境変数にて設定したDBのdatabase
entities table作成の元となるentityファイルを指定。ビルド後に吐き出されるjsファイルを指定。指定したファイルを元にテーブルが作成される。今回はtodoテーブルのみ作成される。
synchronize true指定でentityファイルの変更を即座にDBに反映

本番環境ではsynchronizeオプションはfalseにすること

ファイル保存によって即座にDBへ反映されてしまうため、意図しない変更によりデータの損失につながる可能性があります。開発時は便利なので指定して利用しても良いですが、検証環境や本番環境ではfalseにするのを忘れてないようにしましょう。falseにすることでコマンドが必要になるため能動的にDBを更新できます。

~/src/modules/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; // 追加
import { TypeOrmModule } from '@nestjs/typeorm'; // 追加
import configuration from 'src/config/configuration';

import { TodoModule } from './todo/todo.module';

@Module({
  imports: [
    TodoModule,
    ConfigModule.forRoot({
      envFilePath: '.development.env',
      load: [configuration],
    }),
    // 追加
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => {
        return {
          type: 'mysql',
          host: configService.get('db.host'),
          port: configService.get('db.port'),
          username: configService.get('db.user'),
          password: configService.get('db.password'),
          database: configService.get('db.name'),
          entities: ['dist/**/*.entity.js'],
          synchronize: true,
        };
      },
      inject: [ConfigService], // 追加
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

まとめ

設定が完了したらサーバーを一度終了して立ち上げ直しましょう。ログにLOG [NestApplication] Nest application successfully startedのように表示されればDB接続が成功しています。 上手く接続できない人は下記を試してみましょう。接続できたら次のChapter「DBから取得したデータを表示する」へ進みましょう。

  • NestJSサーバーのログから原因箇所を探す
  • MySQLの設定が環境変数に間違いなく反映できているか調べる
  • @nestjs/typeorm@nestjs/config等インストールされているか確かめる
  • 「configuration.ts」の環境変数参照の仕方が合っているか見直す
  • dockerを使用している方はDBの接続情報が環境変数と一致している見直す

Lesson 3 Chapter 10
DBから取得したデータを表示する

前回DBへの接続が完了したため次はDBからデータを取得します。画面の実装は行わないため、Thunder ClientでDBのデータを参照できたらOKとします。

テストデータのインサート

データの参照にはテストデータが必要なので下記コマンドでデータをインサートします。

MySQLへの接続

MySQLへテストデータをインサートするにはターミナルにてmysql -u root -p等を使いMySQLコンソールへ接続するか、MySQLクライアント(MySQL Workbench etc...)を使用します。

INSERT

mysql
INSERT INTO todo VALUES
  (UUID(),'test1', 0, NOW(), NOW()),
  (UUID(),'test2', 0, NOW(), NOW()),
  (UUID(), 'test3', 1, NOW(), NOW());

SELECT

先ほどのデータがDBに存在するか下記コマンで確認しておきます。3レコード分データが存在していればOKです。

mysql
SELECT * FROM todo;

Todo一覧の取得

この章ではTodo一覧の取得をゴールとします。

Todo Module

「todo.module.ts」内でRepositoryに関わる箇所をコメントアウトしていたためコメントアウトを解除します。またTodoテーブルのデータを扱えるようTypeORMModuleをimportに追加します。

~/src/modules/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { TodoController } from 'src/controllers/todo/todo.controller';
import { Todo } from 'src/entities/todo.entity';
import { TodoRepository } from 'src/repositories/todo.repository';
import { TodoService } from 'src/services/todo/todo.service';

@Module({
  imports: [TypeOrmModule.forFeature([Todo])],
  controllers: [TodoController],
  providers: [TodoService, TodoRepository],
})
export class TodoModule {}

Todo Service

TodoModuleと同じくRepositoryに関わる部分のコメントアウトを解除します。

~/src/services/todo/todo.service.ts
...省略
import { TodoRepository } from 'src/repositories/todo.repository'; // コメントアウト解除

@Injectable()
export class TodoService {
  constructor(private todoRepository: TodoRepository) {} // コメントアウト解除
...省略

Todo一覧取得用のメソッドを実装します。RepositoryによってEntity経由でDBからデータを取得します。また、返り値型を修正します。

Repositoryの返り値型

Repository組み込みのfindメソッドの返り値はPromise<Entity[]>型です。

~/src/services/todo/todo.service.ts
...省略

@Injectable()
export class TodoService {
  constructor(private todoRepository: TodoRepository) {}
  // 変更
  async findAll(): Promise<FindAllTodoResponseDTO> {
    const todos = await this.todoRepository.find();
    return { todos };
  }

  ...省略

}

Todo Controller

Serviceに合わせてControllerも修正します。

~/src/controllers/todo/todo.controller.ts
...省略
@Controller('todo')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}
  @Get()
  // 修正
  async findAll(): Promise<FindAllTodoResponseDTO> {
    return await this.todoService.findAll();
  }

...省略
}

おさらい:3層アーキテクチャ

「Lesson1 Chapter4 MVCモデルとは」にて3層アーキテクチャを学習しました。データはこのアーキテクチャに基づいて流れるため改めて図で復習しておきましょう。

arch_lesson3_ch6.png 3層アーキテクチャ図

Todo一覧の取得

実際にTodo一覧を取得しましょう。サーバーを落としている方はサーバーの再起動が必要です。サーバーの起動後「http://localhost:3000/todo」へGETリクエストをThunder Clientで飛ばしましょう。下記のような形でTodo一覧が取得できれば成功です。

ポート番号について

実際のポート番号は環境によって異なる可能性があります。NestJSのポート番号は「main.ts」で設定可能なのでそちらを参照・変更しリクエストを飛ばしてください。たとえばawait app.listen(3090);のように設定することで「http://localhost:3090/todo」へリクエストを可能になります。

Thunder Client
 {
  "todos": [
    {
      "id": "4c10a231-8a80-11ed-be75-0242c0a8e003",
      "title": "test1",
      "isCompleted": false,
      "createdAt": "2023-01-02 09:31:55",
      "updatedAt": "2023-01-02 09:31:55"
    },
    {
      "id": "8a7745e6-8a80-11ed-be75-0242c0a8e003",
      "title": "test2",
      "isCompleted": false,
      "createdAt": "2023-01-02 09:33:40",
      "updatedAt": "2023-01-02 09:33:40"
    },
    {
      "id": "8a77deb0-8a80-11ed-be75-0242c0a8e003",
      "title": "test3",
      "isCompleted": true,
      "createdAt": "2023-01-02 09:33:40",
      "updatedAt": "2023-01-02 09:33:40"
    }
  ]
}

Todo詳細の取得

一覧が取得できたのでTodo詳細取得の実装を行います。

Todo Service

serviceのfindOneメソッドを変更します。find同様Promiseを返します。またfindOneByを使用してidをキーに一意なTodoを特定します。

~/src/services/todo/todo.service.ts
...省略
constructor(private todoRepository: TodoRepository) {}
  async findAll(): Promise<FindAllTodoResponseDTO> {
    const todos = await this.todoRepository.find();
    return { todos };
  }

  // 変更
  async findOne(id: string): Promise<FindOneTodoResponseDTO> {
    const todo = await this.todoRepository.findOneBy({ id });
    return todo;
  }

...省略

Todo Controller

同じくControllerも下記のように修正します。

~/src/controllers/todo/todo.controller.ts
...省略
constructor(private readonly todoService: TodoService) {}
  @Get()
  async findAll(): Promise<FindAllTodoResponseDTO> {
    return await this.todoService.findAll();
  }

  // 修正
  @Get(':id')
  async findOne(
    @Param() { id }: FindOneTodoRequestDTO,
  ): Promise<FindOneTodoResponseDTO> {
    return await this.todoService.findOne(id);
  }
...省略

Todo詳細の取得

準備が整ったのでTodo詳細をリクエストを飛ばして取得します。たとえば下記のように取得できていたらOKです。

1. まずはキーになるidをTodo一覧の中からコピーします。

2. 取得したidを元に「http://localhost:3090/todo/:id」へGETリクエストを送信します。「:id」へは1でコピーしたidを貼り付けます。

thunder_ch6-1.png Thunder Client Todo詳細取得

まとめ

以上でDBからのデータ取得の基礎は完了です。データ取得のための最低限の実装を終えることができました。次は新しいデータを作成するDBへのインサートを実施します。

Lesson 3 Chapter 11
画面から入力した内容を登録する

新規Todoを登録し、そのデータを参照できることをゴールとします。

新規Todo登録処理の流れ

  1. クライアントからPOSTリクエスト
  2. Controllerでリクエストを受け取り、Serviceを呼び出す
  3. ServiceからRepositoryを呼び出す
  4. Repositoryにてデータを登録し、Serviceに結果を渡す
  5. ServiceからControllerへレスポンスデータを渡す
  6. Controllerからレスポンスをクライアントへ返す

新規Todo登録

入力画面は用意していないためThunder ClientからPOSTリクエストを送信し、新規TodoをDBへ登録します。

カスタムRepositoryの作成

RepositoryはDB操作のためのメソッドを数多く所持していますが、それらのメソッドを上書きしてオリジナルのDB操作用メソッドを作成できます。DTOに含まれないパラメーターをRepositoryで設定する必要があるため、今回はTodo登録用のメソッドを上書きします。

TodoのidにUUIDを設定する必要があるためインストールします。

~/nest-todo
npm i uuid

  • createというメソッドにオブジェクトを引数で渡すことで新しいTodoを作成できます。createTodoRequestDTOからtitleとisCompletedを、idと日付に関する項目はそれぞれ設定したものを引数に渡します。
  • saveメソッドを呼ぶことで実際にDBに登録されます。saveを呼ばない場合ステータス201で正常にレスポンスが返るもののDBにデータの登録はされないので、忘れないように注意が必要です。
repositories/todo/todo.repository.ts
import { Repository } from 'typeorm';
// 追加
import { v4 as uuidv4 } from 'uuid';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { Todo } from 'src/entities/todo.entity';
import { CreateTodoRequestDTO } from 'src/requests/todo';

@Injectable()
export class TodoRepository extends Repository<Todo> {
  constructor(@InjectRepository(Todo) repository: Repository<Todo>) {
    super(repository.target, repository.manager, repository.queryRunner);
  }

  // 追加
  async createTodo(createTodoRequestDTO: CreateTodoRequestDTO): Promise<Todo> {
    const { title, isCompleted } = createTodoRequestDTO;
    const newTodo = this.create({
      id: uuidv4(),
      title,
      isCompleted: !!isCompleted,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    });
    await this.save(newTodo);
    return newTodo;
  }
}

ServiceからRepositoryの利用

ServiceからカスタムRepository内で作成したメソッドを呼び出します。

/services/todo/todo.service.ts
...省略

 // 変更
 async createTodo(
    createTodoDTO: CreateTodoRequestDTO,
  ): Promise<CreateTodoResponseDTO> {
    return await this.todoRepository.createTodo(createTodoDTO);
  }

...省略

ControllerからServiceの利用

ControllerからServiceを呼び出します。

/controllers/todo/todo.controller.ts
...省略

// 変更
@Post()
  async createTodo(
    @Body() createTodoDTO: CreateTodoRequestDTO,
  ): Promise<CreateTodoResponseDTO> {
    return await this.todoService.createTodo(createTodoDTO);
  }

...省略

リクエストを送信する

実装が完了したため実際にリクエスト送信して確認します。Thunder ClientからDTOにしたがってパラメーターを送信しましょう。

/requests/todo/create.dto.ts
export class CreateTodoRequestDTO {
  title: string;
  isCompleted?: boolean;
}

画像のようにリクエストボディを設定しましょう。ポート番号は3090で設定していますが、「main.ts」で指定している自分のポート番号でリクエストしてください。準備ができたら「Send」を押下しましょう。

thunder_ch7-1.png Todo新規登録

レスポンスの一覧の中に作成されたTodoが含まれているのを確認できればOKです。

まとめ

Todoを新規登録しそのデータを参照するところまで完了しました。次は登録したデータに更新をかけていきます。

Lesson 3 Chapter 12
画面から入力した内容で更新する

新規登録したTodoを更新し、参照できることをゴールとします。

Todo更新処理の流れ

  1. クライアントからPUTリクエスト
  2. Controllerでリクエストを受け取り、Serviceを呼び出す
  3. ServiceからRepositoryを呼び出す
  4. Repositoryにてデータを更新し、Serviceに結果を渡す
  5. ServiceからControllerへレスポンスデータを渡す
  6. Controllerからレスポンスをクライアントへ返す

Todo更新

入力画面は用意していないためThunder ClientからPUTリクエストを送信し、既存のTodoデータを更新します。

更新リクエスト用DTO作成

Todo更新リクエスト用とレスポンス用のDTOを作成します。

requests/todo/update.dto.ts
export class UpdateTodoRequestDTO {
  title?: string;
  isCompleted?: boolean;
}
response/todo/update.dto.ts
export class UpdateTodoResponseDTO {
  id: string;
  title: string;
  isCompleted: boolean;
  createdAt: string;
  updatedAt: string;
}

ルートファイルからどちらも吐き出すようにします。

requests/todo/index.ts
// 追加
export * from './update.dto';
export * from './create.dto';
export * from './find.dto';
response/todo/index.ts
// 追加
export * from './update.dto';
export * from './create.dto';
export * from './find.dto';

カスタムRepositoryの作成

Todo更新用のカスタムRepositoryを作成します。PUTによるリクエストを行うためパスパラメーターに該当Todoのidを含みます。そのためidはDTOとは別に受け取ります。

  1. DTOからtitle, isCompletedを取得
  2. 該当のTodoをDBから取得
  3. 更新済みのTodoオブジェクトを作成。その際にupdatedAtを更新
  4. updateメソッドにより既存のTodoを上書き
  5. 更新済みのTodoを返却
repositories/todo/todo.repository.ts
// 追加
  async updateTodo(
    id: string,
    updateTodoDTO: UpdateTodoRequestDTO,
  ): Promise<Todo> {
    const { title, isCompleted } = updateTodoDTO;
    const currentTodo = await this.findOneBy({ id });
    const updatedTodo: Todo = {
      ...currentTodo,
      title,
      isCompleted: !!isCompleted,
      updatedAt: new Date().toISOString(),
    };
    await this.update(id, updatedTodo);
    return updatedTodo;
  }

ServiceからRepositoryの利用

ServiceからカスタムRepository内で作成したメソッドを呼び出します。

/services/todo/todo.service.ts
...省略

  // 追加
  async updateTodo(
    id: string,
    updateTodoDTO: UpdateTodoRequestDTO,
  ): Promise<Todo> {
    return this.todoRepository.updateTodo(id, updateTodoDTO);
  }

...省略

ControllerからServiceの利用

ControllerからServiceを呼び出します。PUTメソッドかつidをパスパラメーターに含めるため@Put(':id')デコレーターを使用します。

/controllers/todo/todo.controller.ts
...省略

@Put(':id')
  async updateTodo(
    @Param() param: { id: string },
    @Body() updateTodoDTO: UpdateTodoRequestDTO,
  ): Promise<UpdateTodoResponseDTO> {
    return await this.todoService.updateTodo(param.id, updateTodoDTO);
  }

...省略

リクエストを送信する

実装が完了したため実際にリクエスト送信して確認します。Thunder ClientからDTOにしたがってパラメーターを送信しましょう。たとえば以下のようになります。準備ができたら「Send」を押下してリクエストを飛ばしましょう。200レスポンスが返り、Todo一覧取得用のAPI等で更新が確認できればOKです。

thunder_ch8-1.png Todo更新

まとめ

Todoを更新しそのデータを参照するところまで完了しました。次は登録したデータの削除を実施します。

Lesson 3 Chapter 13
選択したデータを削除する

「Chapter6 DBから取得したデータを表示する」でTodoの詳細を取得する方法を学びました。その際はTodoのIDより一意なTodoを参照しました。DELETEも同じ方法でTodoを特定し削除します。すでに登録済みのTodoのIDを用いてTodoを削除することをゴールとします。

Todo削除処理の流れ

  1. クライアントからDELETEリクエスト
  2. Controllerでリクエストを受け取り、Serviceを呼び出す
  3. ServiceからRepositoryを呼び出す
  4. Repositoryにてデータを削除し、Serviceに結果を渡す
  5. ServiceからControllerへレスポンスデータを渡す
  6. Controllerからレスポンスをクライアントへ返す

Todo削除

入力画面は用意していないためThunder ClientからDELETEリクエストを送信し、既存のTodoデータを削除します。

削除リクエスト用DTO作成

Todo削除リクエスト用とレスポンス用のDTOを作成します。リクエストはTodoのidをキーにして削除します。また、Responseとして削除後のTodo一覧を返します。

requests/todo/delete.dto.ts
export class DeleteTodoRequestDTO {
  id: string;
}
requests/todo/index.ts
export * from './delete.dto';
export * from './update.dto';
export * from './create.dto';
export * from './find.dto';
response/todo/delete.dto.ts
import { Todo } from 'src/entities/todo.entity';

export class DeleteTodoResponseDTO {
  todos: Todo[];
}
requests/todo/index.ts
export * from './delete.dto';
export * from './update.dto';
export * from './create.dto';
export * from './find.dto';

ServiceからRepositoryの利用

ServiceからRepositoryメソッドを呼び出します。今回はデータに関する特別な操作がないのでカスタムRepositoryを作成する必要はありません。また、データを削除した後に一覧のデータを返すためdeleteメソッドの後にfindメソッドを実行しています。

services/todo/todo.service.ts
...省略

  async deleteTodo(id: string): Promise<DeleteTodoResponseDTO> {
    await this.todoRepository.delete({ id });
    const todos = await this.todoRepository.find();
    return { todos };
  }

...省略

ControllerからServiceの利用

ControllerからServiceを呼び出します。今回は物理削除を行うためDELETEメソッドを利用します。また、idをパスパラメーターに含めるため@Delete(':id')デコレーターを使用します。

物理削除と論理削除

物理削除は実際にDBからデータ(レコード)を削除します。データのバックアップを取ってない限り削除したデータを戻すことはできません。クエリ文はDELETEを使用します。
論理削除は実際にDBからデータ(レコード)を削除しません。実際に削除しないので簡単に削除前の状態に戻せます。削除したかどうかを判定する真偽値型のカラムを用意して値を反転して削除済かどうか判断します。クエリ文はUPDATEを使用します。

...省略

  @Delete(':id')
  async deleteTodo(
    @Param() param: DeleteTodoRequestDTO,
  ): Promise<DeleteTodoResponseDTO> {
    return await this.todoService.deleteTodo(param.id);
  }

...省略

リクエストを送信する

実装が完了したため実際にリクエスト送信して確認します。Thunder ClientからDTOにしたがってパラメーターを送信しましょう。たとえば以下のようになります。準備ができたら「Send」を押下してリクエストを飛ばしましょう。200レスポンスが返り、データ削除ずみのTodo一覧返却が確認できればOKです。

thunder_ch3.8-1.png Todo削除

まとめ

これでNestJSによる基本的なCRUD操作を一通り学習しました。今回行った内容はNestJSによるCRUD操作の基礎中の基礎になります。最低限の構成で行うとこのようになるよというものを紹介しました。今後はこれを基盤に実装を加えていくので理解が怪しい方はしっかりと復習してから臨みましょう。

Lesson 3 Chapter 14
ファイルを添付する

このChapterではTodoのイメージ画像をクライアントから送信して、プロジェクト直下の「files/」ディレクトリに保存する流れを実装します。今回の実装の目的はファイルを送信して、保存、保存先のURLをクライアントに返すというファイルをアップロードする際の流れを学習することです。実際の現場ではAWSのS3等のストレージサービスに保存するのが一般的に用いられますが今回の目的とは逸れるため保存先は簡易的に実装します。大きな処理の流れは下記になります。

処理順

  1. 画像ファイルをクライアントから送信する
  2. サーバーサイドで受け取った画像ファイルを「プロジェクト/files/」に保存する
  3. 画像の保存先URLをクライアントに返却する

実装のための準備

実装のための準備として以下のことを行います。

  • @types/multerインストール
  • Todo EntityにimgUrlを追加

@types/multerインストール

NestJSでファイルのアップロードを実装するためにMulterというモジュールを用います。このモジュールは公式でも推奨されており広く利用されています。MulterをTypeScriptで利用するために型定義ファイルをインストールします。

nest-todo/
npm i -D @types/multer

Todo EntityにimgUrlを追加

画像の保存先URLをDBに格納する必要があるためTodoテーブルにカラムを追加します。テーブルにカラムを追加するためにはTodoEntityクラスにプロパティの追加が必要です。以下のように追加しましょうEntityの更新はTypoORMモジュールのsynchronize: trueにより自動的にDBに反映されます。

~/src/entities/todo.entity.ts
@Entity()
export class Todo {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 128, nullable: false })
  title: string;

  @Column({ default: false, nullable: false })
  isCompleted: boolean;

  // 追加
  @Column({ length: 256, nullable: true })
  imgUrl: string;

  @Column({ nullable: false })
  createdAt: string;

  @Column({ nullable: false })
  updatedAt: string;
}

MySQLを起動し、以下のコマンドでimgUrlカラムが追加されているか確認しましょう。

MySQL
SHOW COLUMNS FROM todo;

画像アップロード処理実装

実際のアップロード処理を実装していきます。画像アップロード処理の流れは下記になります。

  1. controllerで画像をストレージに保存、保存先URLをserviceに渡す
  2. serviceからrepositoryに保存先URLを渡す
  3. repositoryで保存先URLをDBに保存

controllerで画像を保存

画像を保存するにはMulterモジュールとNestJSのInterceptorを用いる必要があります。Interceptorは「Lesson4 Chapter5 共通エラー処理の実装」で詳しく解説しています。今回は趣旨とはズレるため詳しくは解説せず簡単な説明に抑えます。

Multerモジュール

Multerモジュールはmultipart/form-data形式でポストされたファイルデータを参照して任意の処理ができるモジュールです。とても使いやすく設計されておりアプリケーションの要件に合わせて動作を調整することができます。

Interceptor

InterceptorはNestJSの機能で、リクエストとレスポンスの間に共通処理を挟むことができます。今回はFileInterceptorというNestJSが標準で持つInterceptor用のメソッドを用いて実装します。

実装に入っていきましょう。「todo.controller.ts」内createTodoを下記のように変更しましょう。新たに使用するモジュール群をimportします。また、メソッドに対して@UseInterceptorsデコレーターを用いてFileInterceptorを適用し、ファイルを抽出します。ファイルを抽出後、MulterからdiskStorageメソッドを使って宣言的にファイルの保存先とファイル名の指定が可能です。destination: './files'でファイルの保存先、filename: ~メソッドでファイル名を指定し、filenameのコールバック関数の第2引数に文字列を渡すことでファイル名が決定します。controllerの引数に保存済みのfileオブジェクトが渡されるため@UploadedFileデコレーターにより取得します。

todo.controller.ts
...省略
// 追加のimport
import { Request } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import {
  ...省略
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';

...省略

  @Post()
  // Interceptor追加
  @UseInterceptors(
    FileInterceptor('file', { // ファイルの保存処理
      storage: diskStorage({
        destination: './files', // 保存先を指定
        filename: (               // ファイル名の指定
          req: Request,
          file: Express.Multer.File,
          cb: (error: Error | null, filename: string) => void,
        ) => cb(null, file.originalname),
      }),
    }),
  )
  async createTodo(
    @Body() createTodoDTO: CreateTodoRequestDTO,
    @UploadedFile() file: Express.Multer.File, // 保存済みのファイルを取得
  ): Promise<CreateTodoResponseDTO> {
    console.log(file);
    return await this.todoService.createTodo(createTodoDTO);
  }

POSTリクエストで画像を送信する

POSTリクエストで画像を送信します。下記のように画像ファイルを指定し、リクエストしてみましょう。画像は好きなものを画像ファイル形式はpngやjpg等を送信してみましょう。

postman_ch10-1.png postmanより画像をPOSTリクエスト

例えば以下のようなレスポンスが返却されれば画像の送信は成功しています。

コンソール
{
  fieldname: 'file',
  originalname: 'arch_lesson1_ch4-1.png',
  encoding: '7bit',
  mimetype: 'multipart/form-data',
  destination: './files',
  filename: 'arch_lesson1_ch4-1.png',
  path: 'files/arch_lesson1_ch4-1.png',
  size: 76313
}

また、「./files」配下に画像が保存されていることも確認できます。

dir_ch10-1.png

controller処理の切り分け

Interceptorのファイル名のロジック等切り分けれるものを別ファイルに切り出しファイル内の見通しを良くしていきます。ひとつはファイル保存先のディレクトリへのパスです。現在'./files'と記述していますが他のファイルからもこの値を参照したいときや将来的に変わる可能性があるかもしれません。その時に変更範囲を抑えることができるように定数に切り出します。具体的には「src」配下に「constants」というディレクトリを切りその中に格納します。

~/src/
mkdir constants && cd constants
touch index.ts

ディレクトリとファイルを作成したので値を追加します。

~/src/constants/index.ts
export const TODO_IMAGE_FILE_PATH = './files';

実際にcontrollerで使用します。

~/src/controllers/todo.controller.ts
...省略
import { TODO_IMAGE_FILE_PATH } from 'src/constants'; // 追加

...省略

storage: diskStorage({
  destination: TODO_IMAGE_FILE_PATH,
  ...省略
)}

...省略

次にfilenameのロジックを切り出します。切り出す理由としてはcontrollerにfilename決定のロジックの責務を持たせたくないからです。切り出してモジュールに担当させましょう。後のChapterでも使用しますが「src」配下に「interceptors/todo」ディレクトリを切ってその中でモジュールを作成しましょう。

~/src/
mkdir interceptors && cd interceptors
mkdir todo && cd todo
touch image-file.interceptor.ts

ファイルの作成後、処理を記述しましょう。また、controllerに直接記述していたときは送信されたファイル名を直接使用していましたが今後被る可能性が高いため被らないようにファイル名を生成するロジックを追加しました。

~/src/interceptors/todo/image-file.interceptor.ts
import { Request } from 'express';
import { extname } from 'path';

export const generateFilename = (
  req: Request,
  file: Express.Multer.File,
  cb: (error: Error | null, filename: string) => void,
) => {
  const name = file.originalname.split('.')[0];
  const fileExtName = extname(file.originalname);
  const randomName = Array(4)
    .fill(null)
    .map(() => Math.round(Math.random() * 16).toString(16))
    .join('');
  cb(null, `${name}-${randomName}${fileExtName}`);
};

controllerに作成したモジュールを適用しましょう。Interceptor部分がかなりスッキリして見やすくなりました。また、ファイル名のロジックなどはもうジュールに隠蔽されてcontrollerからは気にする必要がなくなりました。

todo.controller.ts
...省略

  @Post()
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: TODO_IMAGE_FILE_PATH,
        filename: generateFilename,
      }),
    }),
  )
  async createTodo(
    ...省略

DBに画像のURLを保存する

画像を保存することができたので画像のURLをDBに保存する処理を追加しましょう。controllerとserviceとrepositoryの処理を追記していきます。controllerにはserviceに画像のURLを渡す処理を追加しました。また、BASE_URLとして保存先までのベースURLをconstantsに定義して使用しています。

constants/index.ts
export const BASE_URL = 'http://localhost:3090'; // 追加
todo.controller.ts
...省略

import { BASE_URL // 追加, TODO_IMAGE_FILE_PATH } from 'src/constants';

...省略

  async createTodo(
    @Body() createTodoDTO: CreateTodoRequestDTO,
    @UploadedFile() file: Express.Multer.File,
  ): Promise<CreateTodoResponseDTO> {
    // 追加
    const imagePath = `${BASE_URL}/${file.path}`;
    return await this.todoService.createTodo(createTodoDTO, imagePath);
  }

...省略

serviceでも受け取るための処理を追加します。

todo.service.ts
...省略

async createTodo(
    createTodoDTO: CreateTodoRequestDTO,
    imagePath: string, // 画像のURL取得
  ): Promise<CreateTodoResponseDTO> {
    // 第二引数でrepositoryへ渡す
    return await this.todoRepository.createTodo(createTodoDTO, imagePath);
  }

...省略

repository内で画像URlの保存処理を実行します。

todo.repository.ts
...省略

async createTodo(
    createTodoRequestDTO: CreateTodoRequestDTO,
    imagePath: string, // 画像のURL取得
  ): Promise<Todo> {
    const { title, isCompleted } = createTodoRequestDTO;
    const newTodo = this.create({
      id: uuidv4(),
      title,
      isCompleted: !!isCompleted,
      imgUrl: imagePath, // 画像のURL保存
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    });
    await this.save(newTodo);
    return newTodo;
  }

...省略

ここまでできたところでもう一度画像を送信してみましょう。下記のようにimgUrlが含まれたレスポンスが返れば成功です。

POST /todo Response
{
    "id": "71ea60cf-e0e4-4db4-bdc7-44a777b2f23d",
    "title": "file send test",
    "isCompleted": true,
    "imgUrl": "http://localhost:3090/files/arch_lesson1_ch4-1-3724.png",
    "createdAt": "2023-01-08T06:11:08.870Z",
    "updatedAt": "2023-01-08T06:11:08.871Z"
}

まとめ

以上で「ファイルを添付する」Chapterおよび「MySQLとの接続」Lessonは終了となります。このChapterではInterceptorを使ってファイルを保存しましたが、保存先とファイル名の指定のみの最小構成で利用しました。Multerにはファイルのバリデーションをかけられたりサイズの上限を決めることができたりと様々な機能が存在するので興味がある方は調べてみてください。実際の現場で利用する際はファイル形式のバリデーションやサイズの上限値を決めて利用するのが一般的です。