NestJSをゼロから学ぶ - TypeORMの活用などをREST APIの実装から身に付けよう【Node.jsフレームワークの基本】
Nest.jsは、スケーラブルで効率的なサーバーサイドのNode.jsフレームワークで、TypeScriptで構築されています。この記事はNestJSのハンズオンとして、TypeScriptやNode.jsの経験があるソフトウェアエンジニアが手を動かしながらNestJSに入門できます。NestJS Japan Users Groupの羽馬直樹さんによる執筆です。
はじめまして。羽馬(@NaokiHaba)と申します。株式会社エブリーのDELISH KITCHEN 開発本部でバックエンドエンジニアをしています。プライベートでは、NestJS Japan Users Groupの運営を通じてNestJSの普及に貢献しています。
当記事ではハンズオンを通して、初めてNestJSを触る方がNestJSの基本的な機能を学べるように、REST APIを作成しながら解説していきます。読み終えた方は、次のようなことができるようになっていることを想定しています。
- NestJSの基本的な機能を使うことができる
- NestJSでREST APIを作成することができる
- NestJS + JestでREST APIのテストを書くことができる
対象読者には、次のような方を想定しています。
- Node.jsを使った開発経験はある
- TypeScriptを使った開発経験はある
- NestJSを使った開発経験はない
これはあくまで想定読者であり、経験のない方でもNestJSの基本的な機能を学べるよう解説していきます。
- この記事のハンズオンで必要なソフトウェア
- NestJSはどういうフレームワークか?
- NestJS CLIで環境構築を行う
- 実装するREST APIについて
- データベースと接続してテーブルを作成
- ユーザーを作成するAPIの実装
- ユーザー情報を取得するAPIの実装
- ユーザーを更新・削除するAPIの実装
- まとめ
この記事のハンズオンで必要なソフトウェア
この記事でハンズオンを実施する前に、次の準備をしておいてください。
- Node.jsのインストール
- Dockerのインストール
- Docker Composeのインストール
Node.jsは、バージョン16.14.0で動作確認しています。次のコマンドでバージョンを確認できます。
$ node -v v16.14.0
Dockerは、バージョン20.10.17で動作確認しています。次のコマンドで確認できます。
$ docker -v Docker version 20.10.17, build f0df350
Docker Composeは、バージョン2.6.1で動作確認しています。次のコマンドで確認できます。
$ docker-compose -v docker-compose version 2.6.1
そのほか次の環境で動作確認を行っています。
- NestJS 9.1.5
- MySQL 8.0
- macOS Ventura 13.1
また、解説するソースコードは次のGitHubリポジトリで公開しています。
それでは、NestJSの基本的な機能を学んでいきましょう。
NestJSはどういうフレームワークか?
NestJSとは、Node.jsのフレームワークのひとつで、TypeScriptで構築されています。コマンドラインのインターフェースであるNestJS CLIを使うと、プロジェクトの作成からテストまで簡単に行うことができます。
▶ NestJS - A progressive Node.js framework(公式サイト)
そのほか次のような特徴があります。
- GraphQLやマイクロサービスなど、さまざまな機能をサポート
- FastifyやExpressなどのWebフレームワークをサポート
- AngularやReactなど、さまざまなフロントエンドフレームワークと連携が可能
- テストツールに依存せず、JestやSuperTestとの統合が容易
- 公式のドキュメントが充実している
このようにNestJSは多機能で、また次のようにコミュニティも活発なフレームワークです。
- 2023年1月時点で、GitHubのスター数が約53,600個
- NestJS Japan Users Groupが運営されている
- NestJS公式Discordがある
なお、Node.jsのWebフレームワーク中で最も人気が高いのがExpressで、FastifyはそのExpressより高速なフレームワークです。
NestJSのファイル構成
NestJSのファイル構成と、利用されるコアファイルを説明します。
まず、ファイル構成は次のようになっています。
src/ ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── main.ts
このうちapp.controller.ts
は、次のようなファイルです。
- アプリケーションのルートに対するリクエストを処理するコントローラ
@Controller()
デコレータを使って、ルートを定義する
app.module.ts
は次のようなファイルです。
- アプリケーションのルートモジュール
@Module()
デコレータを使って、モジュールを定義する
app.service.ts
は次のようなファイルです。
- アプリケーションのルートに対するリクエストを処理するサービス
@Injectable()
デコレータを使って、サービスを定義する
main.ts
は、アプリケーションのエントリポイントです。
- アプリケーションのルートモジュール(
app.module.ts
)をインポートする @NestFactory.create()
メソッドを使って、アプリケーションを起動する
デコレータ:NestJSの便利な機能
NestJSには便利な機能が数多くありますが、その中からデコレータ、依存性注入、CLIの3つを紹介します。
デコレータとは、クラスやメソッドに対して、メタデータを付与する仕組みです。NestJSでは、デコレータを使ってコントローラやモジュールを定義します。デコレータを使ってクラスやメソッドにメタデータを付与することで、クラスやメソッドに対してさまざまな機能を付与することができます。
例えば次のようなデコレータが、NestJSには用意されています。
デコレータ | 役割 |
---|---|
@Controller() |
コントローラを定義する |
@Get() |
GETリクエストを処理する |
@Post() |
POSTリクエストを処理する |
@Put() |
PUTリクエストを処理する |
@Delete() |
DELETEリクエストを処理する |
@Patch() |
PATCHリクエストを処理する |
@Module() |
モジュールを定義する |
@Injectable() |
サービスを定義する |
@Inject() |
依存性を注入する |
@Body() |
リクエストボディを取得する |
@Param() |
リクエストパラメータを取得する |
@Query() |
クエリパラメータを取得する |
@HttpCode() |
HTTPステータスコードを定義する |
このようにさまざまなデコレータが用意されていますが、今回はサンプルで利用するデコレータに絞って説明します。他のデコレータについては、NestJSの公式ドキュメントなどを参照してください。
依存性注入(DI)
依存性注入(DI、Dependency Injection)とは、クラスの依存関係を解決する仕組みです。NestJSでは、依存性注入を使ってクラスの依存関係を解決します。
クラスの依存関係とは、クラスが他のクラスに依存している関係のことです。例えば、次のようなクラスAがあったとします。このクラスは他のクラスに依存していません。@Injectable()
デコレータを使って、サービスを定義します。
@Injectable() class A { constructor() {} }
そして、次のようなクラスBがあったとします。このクラスは、@Inject()
デコレータを使って依存性を注入しており、クラスAに依存しています。
@Injectable() class B { constructor(@Inject(A) private a: A) {} }
このクラスBをインスタンス化するには、クラスAのインスタンスが必要です。インスタンス化に必要なクラスAのインスタンスを、クラスBのコンストラクタで受け取っています。このようにクラスのコンストラクタで、インスタンス化に必要な別のクラスのインスタンスを受け取ることを依存性注入と呼びます。
NestJS CLI
NestJSには、CLIツールが用意されています。CLIツールとは、コマンドラインからアプリケーションを作成したりテストを実行したりするツールのことです。
NestJS CLIを使うことで、次のようにさまざまなことが実行できます。
- アプリケーションの作成
- モジュールの作成
- コントローラの作成
- サービスの作成
- テストの実行
- ビルド
公式ドキュメントも参照しよう
ここまで、NestJSの基本的な機能を簡単に紹介してきました。こういった解説だけではイメージが湧きにくいかもしれませんが、これから実際にアプリケーションを作成していくと、イメージがつかめると思います。この段階では、NestJSのファイル構成をざっくりと把握しておけばよいでしょう。
次のセクションから、実際に環境構築を行って、アプリケーションを作成していきます。より詳細な説明が必要なときには、NestJS公式のドキュメントなども参照してください。
▶ Documentation | NestJS
NestJS CLIで環境構築を行う
このセクションでは、NestJSの環境構築を行っていきます。次の2つの方法があります。
- NestJS CLIを使う
- 手動で環境構築する
今回はNestJS CLIを使います。まず次のコマンドを実行して、NestJS CLIそのものをインストールします。
$ npm i -g @nestjs/cli
-g
オプションを付けると、グローバルにインストールします。これで、コマンドラインからNestJS CLIを実行できるようになります。
アプリケーションのプロジェクトを作成
NestJS CLIを使って、アプリケーションを作成するには、次のコマンドを実行します。<アプリケーション名>
には、作成したい任意のアプリケーションの名前を指定します。
$ nest new <アプリケーション名>
今回は次のように、nestjs-example
という名前でアプリケーションを作成します。
$ nest new nestjs-example
実行すると、次のメッセージが表示されます。
Which package manager would you ❤️ to use? npm ✔ Installation in progress... ☕ 🚀 Successfully created project nestjs-example 👉 Get started with the following commands: $ cd nestjs-example $ npm run start
そして、次のファイル構成でアプリケーションが作成されます。
nestjs-example/
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── src/
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ └── test
│ └── app.e2e-spec.ts
├── tsconfig.build.json
├── tsconfig.json
作成したディレクトリに移動して、次のコマンドでアプリケーションを起動しましょう。
$ cd nestjs-example
$ npm run start:dev
起動時にnpm run start:dev
と実行することで、ホットリロードを有効にした状態でアプリケーションを起動できます。ホットリロードとは、ファイルを変更した際にアプリケーションを再起動する必要がなくなる機能のことです。
このコマンドを実行すると、次のメッセージが表示されます。
[Nest] 13473 - 01/15/2023, 3:09:23 AM LOG [NestFactory] Starting Nest application... [Nest] 13473 - 01/15/2023, 3:09:24 AM LOG [InstanceLoader] AppModule dependencies initialized +102ms [Nest] 13473 - 01/15/2023, 3:09:24 AM LOG [RoutesResolver] AppController {/}: +12ms [Nest] 13473 - 01/15/2023, 3:09:24 AM LOG [RouterExplorer] Mapped {/, GET} route +3ms [Nest] 13473 - 01/15/2023, 3:09:24 AM LOG [NestApplication] Nest application successfully started +3ms
この状態でhttp://localhost:3000
にアクセスすると、次のメッセージが表示されます。
Hello World!
これでNestJSの環境構築が完了しました。いかがでしょうか? かなり簡単に環境構築できましたね。
MySQLコンテナをDocker Composeで起動する
ここからはアプリケーションで利用するソフトウェアを用意していきます。まず、データベースとしてMySQLを、Docker Composeを利用してコンテナで起動します。なお、Docker Composeそのものの説明が必要な方は、公式のドキュメントなどを参照してください。
コンテナを起動するために、docker-compose.yml
を作成します。
$ touch docker-compose.yml
今回のdocker-compose.yml
には、次の内容を記述します。
version: '3' services: db: image: mysql:8.0 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci container_name: db_container volumes: - mysql-data-volume:/var/lib/mysql ports: - "3306:3306" environment: TZ: 'Asia/Tokyo' MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: test MYSQL_USER: test MYSQL_PASSWORD: password volumes: mysql-data-volume:
作成できたら、次のコマンドを実行してMySQLコンテナを起動します。-d
オプションを付けて、バックグラウンドで起動します。
$ docker-compose up -d
コンテナが起動すると、次のメッセージが表示されます。
⠿ Container db_container Started 1.1s
docker-compose ps
コマンドを実行すると、次のメッセージが表示されます。
NAME COMMAND SERVICE STATUS PORTS db_container "docker-entrypoint.s…" db running 0.0.0.0:3306->3306/tcp, 33060/tcp 0.0.0.0:3306->3306/tcp, 33060/tcp
これでMySQLコンテナが起動しました。
TypeORMのセットアップ
起動したMySQLに接続するため、ORマッパーとしてTypeORMを利用します。TypeORMそのものの説明は省略しますが、気になる方は公式サイトなどを参照してください。
▶ TypeORM - Amazing ORM for TypeScript and JavaScript
次のコマンドを実行して、TypeORMをインストールします。TypeORMをインストールすると、typeorm
コマンドが使えるようになります。
$ npm install --save @nestjs/typeorm typeorm mysql2
続いてTypeORMの設定ファイルを作成します。先ほどのアプリケーションがあるnestjs-example
ディレクトリに、typeOrm.config.ts
という設定ファイルを作成します。
内容は次のようになります。
import { DataSource } from 'typeorm'; export default new DataSource({ type: 'mysql', host: 'localhost', port: 3306, database: 'test', username: 'test', password: 'password', entities: ['dist/**/entities/**/*.entity.js'], migrations: ['dist/**/migrations/**/*.js'], // ログを出力するかどうか logging: true, // synchronize は開発時にのみ使用する // trueにすると、エンティティの変更を検知して、自動的にテーブルが更新される // 本番環境では、falseにすること });
このうちsynchronize
オプションは、開発環境でのみ使用します。本番環境ではsynchronize
オプションをfalse
にしてください。公式のドキュメントにも、synchronize
オプションをtrue
にすると本番環境のデータを消してしまう可能性があると書かれています。
Setting synchronize: true shouldn't be used in production - otherwise you can lose production data.
データベースとの接続で使用する環境変数
データベースに接続するために必要な情報は、環境変数でアプリケーションに渡します。NestJSで環境変数を取得するライブラリとして、@nestjs/config
を使用します。
次のコマンドを実行してインストールしましょう。
$ npm install --save @nestjs/config
次に、このライブラリの設定ファイルであるconfig/configuration.ts
を作成します。今回は、次のような内容になります。
export default () => ({ database: { host: process.env.DATABASE_HOST || 'localhost', port: parseInt(process.env.DATABASE_PORT, 10) || 3306, username: process.env.DATABASE_USER || 'test', password: process.env.password || 'password', name: process.env.database || 'test', }, });
なお、実行環境がクラウドの場合には、変数はGCPのCloud Runや、AWSのECSから取得することが多いと思います。
環境構築のまとめ
以上がNestJSの環境構築の手順でした。このセクションでの参照URLをまとめておきます。
余談ですが、今回はORMとしてTypeORMを使用しましたが、最近はPrismaを利用する開発者が増えている印象です。Prismaはデータベースから自動でスキーマを生成してくれるので、開発効率が上がるというメリットがあります。個人的にはTypeORMの方が好みですが、Prismaも検討してみてください。
次のセクションでは、実際にNestJSでAPIを作成していきます。
実装するREST APIについて
いよいよNestJSでREST APIを作成していきます。今回は、データベース上のユーザー情報を操作する次のようなAPIを作成します。
メソッド | URL | 説明 |
---|---|---|
GET | /users | ユーザー一覧を取得 |
GET | /users/:id | ユーザーを取得 |
POST | /users | ユーザーを作成 |
PATCH | /users/:id | ユーザーを更新 |
DELETE | /users/:id | ユーザーを削除 |
操作するユーザー情報は、次のようにシンプルなテーブルとします。
カラム | 内容 | 備考 |
---|---|---|
id | ユーザーID | 主キー |
name | ユーザー名 |
CRUD処理のひな形を作成する
NestJS CLI(nest
コマンド)を使用することで、リソースに対するCRUD(Create Read Update Delete)処理を次のように簡単に作成できます。
$ nest g resource users ? What name would you like to use for this resource (plural, e.g., "users")? users ? What transport layer do you use? (Use arrow keys) ❯ REST API GraphQL (code first) GraphQL (schema first) Microservice (non-HTTP) WebSockets ? What transport layer do you use? REST API ? Would you like to generate CRUD entry points? Yes CREATE src/users/users.controller.spec.ts (614 bytes) CREATE src/users/users.controller.ts (1.02 KB) CREATE src/users/users.module.ts (1.02 KB) CREATE src/users/users.service.spec.ts (1.02 KB) CREATE src/users/users.service.ts (1.02 KB) CREATE src/users/users.entity.ts (1.02 KB) CREATE src/users/dto/create-user.dto.ts (1.02 KB) CREATE src/users/dto/update-user.dto.ts (1.02 KB)
ここでは、ユーザーを表すusers
というリソース(resource
)を作成(g
enerate)しています。
このコマンドで、以下のようなCRUD処理のひな形となる基本的なファイルが、nestjs-example/src/users/
ディレクトリに作成されました。
ファイル名 | 説明 |
---|---|
users.controller.ts | ルーティングを定義する |
users.module.ts | モジュールを定義する |
users.service.ts | ビジネスロジックを定義する |
users.entity.ts | データベースのテーブルを定義する |
users.controller.speck.ts | コントローラのテストを定義する |
users.service.speck.ts | サービスのテストを定義する |
create-user.dto.ts | クライアントからの作成リクエストのバリデーションを定義する |
update-user.dto.ts | クライアントからの更新リクエストのバリデーションを定義する |
なお、CLIを使用しなくても同じようなファイルを作成することはできますが、CLIを使用すれば簡単にCRUDを作成できます。
データベースと接続してテーブルを作成
ユーザー情報を保存するMySQLは先ほどコンテナで起動し、このデータベースと接続できるようTypeORMも設定もしています。ここではアプリケーションからTypeORMを利用して、テーブルを作成します。
エンティティを定義する
まずsrc/users/users.entity.ts
を編集して、データベースのテーブルを定義します。
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('users') export class User { @PrimaryGeneratedColumn({ comment: 'アカウントID', }) readonly id: number; @Column('varchar', { comment: 'アカウント名' }) name: string; constructor(name: string) { this.name = name; } }
このコードでは、TypeORMから次のデコレータをインポートしています。
デコレータ | 説明 |
---|---|
@Entity |
データベースのテーブルを定義する |
@PrimaryGeneratedColumn |
主キーを定義する |
@Column |
カラムを定義する |
これで定義されるテーブルの構成は、先に説明したような2カラムのシンプルなものです。
なお、TypeORMのデコレータを詳しく知りたい方は公式ドキュメントを参照してください。
▶ Decorator reference - typeorm/typeorm
TypeORMモジュールにエンティティを登録する
先ほど作成したエンティティをTypeORMモジュールに登録し、両者を紐付けます。
src/users/users.module.ts
を編集します。
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; @Module({ controllers: [UsersController], providers: [UsersService], imports: [TypeOrmModule.forFeature([User])], }) export class UsersModule {}
src/users/users.entity.ts
からインポートしたUser
を、@Module
のTypeOrmModule.forFeature
で登録しています。
TypeORMモジュールをアプリケーションに登録する
次にsrc/app.module.ts
を編集して、TypeORMモジュールをアプリケーションに登録します。
import { ConfigModule, ConfigService } from '@nestjs/config'; import config from '../config/configuration'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; import { AppController } from './app.controller'; import { Module } from '@nestjs/common'; @Module({ imports: [ AppModule, ConfigModule.forRoot({ isGlobal: true, load: [config], }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ type: 'mysql', host: configService.get('database.host'), port: configService.get('database.port'), username: configService.get('database.username'), password: configService.get('database.password'), database: configService.get('database.name'), entities: ['dist/**/entities/**/*.entity.js'], }), inject: [ConfigService], }), UsersModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
TypeORMでマイグレーションを実行する準備
src/users/users.module.ts
で定義したエンティティがTypeORMモジュールに紐付いたので、TypeORMでマイグレーションを実施してMySQLデータベースにテーブルを作成していきます。そのために必要なマイグレーションファイルをまず作成します。
今回はマイグレーションファイルの作成と実行に、TypeORMのCLIツールを使用します。ただし、TypeORMのCLIツールはJavaScriptで書かれているため、今回のようなTypeScriptのファイルには、次のドキュメントに従ってts-nodeを使用する必要があります。
▶ Using CLI - If entities files are in typescript | TypeORM
また、TypeORMのCLIツールをnpmスクリプトとして実行できるよう、nestjs-example
ディレクトリにあるpackage.json
ファイルに以下の通り追記してください。
{ "scripts": { "typeorm": "ts-node ./node_modules/typeorm/cli", "typeorm:run-migrations": "npm run typeorm migration:run -- -d ./typeOrm.config.ts", "typeorm:generate-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:generate ./migrations/$npm_config_name", "typeorm:revert-migration": "npm run typeorm -- -d ./typeOrm.config.ts migration:revert" } }
ここではts-node
によるTypeORM CLI(node_modules/typeorm/cli
)の実行のほか、それを使用したマイグレーションの実行とリバート、マイグレーションファイルの作成を定義しています。
マイグレーションファイルを作成
さっそくマイグレーションファイルを作成します。先ほど記載したtypeorm:generate-migration
をnpmスクリプトとして実行してください。
実行前にdist
ディレクトリを生成しておく必要があります。
再度、npm run start:dev
を実行してください。
$ npm run start:dev
$ npm run typeorm:generate-migration --name=CreateUsers
src/users/users.entity.ts
で定義したテーブルのマイグレーションファイルが、migrations
ディレクトリに作成されます。
マイグレーションを実行してテーブルを作成する
次に、作成したマイグレーションファイルをtypeorm:run-migrations
で実行します。
$ npm run typeorm:run-migrations query: START TRANSACTION query: CREATE TABLE `users` (`id` int NOT NULL AUTO_INCREMENT COMMENT 'アカウントID', `name` varchar(255) NOT NULL COMMENT 'アカウント名', PRIMARY KEY (`id`)) ENGINE=InnoDB query: INSERT INTO `test`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1673728396326,"CreateUsers1673728396326"] Migration CreateUsers1673728396326 has been executed successfully. query: COMMIT
これでデータベースにテーブルが作成されました。 以下のコマンドで確認できます。
$ docker-compose exec db mysql -u root -ppassword -D test -e "DESC users" mysql: [Warning] Using a password on the command line interface can be insecure. +-------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | +-------+--------------+------+-----+---------+----------------+
DTOの作成とバリデーション
続いてDTO(Data Transfer Object)を作成します。 DTOは、データの構造を定義するクラスです。
データ構造をバリデーションするパッケージとして、class-validator
を使用します。
$ npm i --save class-validator class-transformer
これを使用するようにsrc/users/create-user.dto.ts
を編集します。
import { IsNotEmpty, MaxLength } from 'class-validator'; export class CreateUserDto { @MaxLength(255) @IsNotEmpty() name: string; }
ここでは次のデータ構造を定義しています。
デコレータ | 説明 |
---|---|
@MaxLength |
文字数の最大値を定義する |
@IsNotEmpty |
空文字を許可しない |
update-user.dto.ts
もクラス名を除いて同じ内容に編集してください。
import { IsNotEmpty, MaxLength } from 'class-validator'; export class UpdateUserDto { @MaxLength(255) @IsNotEmpty() name: string; }
定義したDTOでバリデーションを行うパイプとしてValidationPipe
を使用するように、main.ts
で設定します。useGlobalPipes
でグローバルに設定できます。
import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
なお、定義できるバリデーションの種類は次のドキュメントを参照してください。
▶ typestack/class-validator: Decorator-based property validation for classes.
ユーザーを作成するAPIの実装
データベースが用意できたので、実際にユーザー情報を操作できるAPIを実装していきましょう。どのようなAPIを作成するかは、前半の「実装するREST APIについて」のセクションを参照してください。
まず始めに、ユーザーを作成するAPIから作成します。
コントローラにPostメソッドを追加
HTTPリクエストを処理するコントローラは、src/users/users.controller.ts
に記述します。
@Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Post() async create(@Body() createUserDto: CreateUserDto): Promise<User> { return await this.usersService.create(createUserDto); } }
記事冒頭の「NestJSはどういうフレームワークか?」で説明したように、ここではUsersController
クラスに@Post
デコレータを追加することで、POSTメソッドを定義しています。@Body
で、リクエストボディを取得しています。
サービスにcreateメソッドを追加
次にsrc/users/users.service.ts
を編集して、ユーザーを作成するロジックを記述していきます。
@Injectable() export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async create({ name }: CreateUserDto): Promise<User> { return await this.userRepository .save({ name: name, }) .catch((e) => { throw new InternalServerErrorException( `[${e.message}]:ユーザーの登録に失敗しました。`, ); }); } }
ここではUsersService
クラスに、create()
メソッドを作成しています。
await this.usersRepository.save(createUserDto);
で、データベースに保存しています。
ユーザーを作成するAPIを実行
いま作成したAPIを実行してみましょう。前述したようにアプリケーションをホットリロードで起動しているため再起動などは必要ありませんが、フロントエンド部分がないためコマンドラインツールでアプリケーションに接続します。
# ユーザーを作成する $ curl -X POST -H "Content-Type: application/json" -d '{"name": "test"}' http://localhost:3000/users {"id":1,"name":"test"} # 戻り値として定義しているUserエンティティが返却される # nameが空文字の場合は、エラーになる $ curl -X POST -H "Content-Type: application/json" -d '{"name": ""}' http://localhost:3000/users {"statusCode":400,"message":"Bad Request","error":"Bad Request"}
サービスのテストを作成
実装が終わったら、ユニットテストをJestで作成していきます。
ユニットテストの役割は、実装したコードが正しく動作しているかを確認することです。ユニットテストを書くことで、コードの品質を保つことができます。積極的にユニットテストを書くようにしましょう。
それではsrc/users/users.service.spec.ts
を編集します。
describe('UsersService', () => { let service: UsersService; beforeEach(async () => { // テスト用のモジュールを作成する const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ UsersService, { provide: getRepositoryToken(User), useClass: Repository, }, ], }).compile(); // テスト用のモジュールから、UsersServiceを取得する service = module.get<UsersService>(UsersService); }); // テストケース describe('create()', () => { it('should successfully insert a user', () => { const dto: CreateUserDto = { name: '太郎', }; jest .spyOn(service, 'create') .mockImplementation(async (dto: CreateUserDto) => { const user: User = { id: 1, ...dto, }; return user; }); expect(service.create(dto)).resolves.toEqual({ id: 1, ...dto, }); }); }); });
テストを実行してみましょう。
$ npm run test PASS src/users/users.service.spec.ts (9.464 s) FAIL src/users/users.controller.spec.ts (9.474 s) ● UsersController › should be defined
サービスのテストは成功していることが確認できました。
コントローラーのテストを作成
次にコントローラーのテストを作成するため、src/users/users.controller.spec.ts
を編集します。
describe('UsersController', () => { let controller: UsersController; let service: UsersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ UsersService, { provide: getRepositoryToken(User), useClass: Repository, }, ], }).compile(); controller = module.get<UsersController>(UsersController); service = module.get<UsersService>(UsersService); }); describe('create()', () => { it('should create a user', () => { const dto: CreateUserDto = { name: '太郎', }; jest .spyOn(service, 'create') .mockImplementation(async (dto: CreateUserDto) => { const user: User = { id: 1, ...dto, }; return user; }); expect(controller.create(dto)).resolves.toEqual({ id: 1, ...dto, }); }); }); })
テストを実行してみましょう。
$ npm run test PASS src/users/users.service.spec.ts (9.464 s) PASS src/users/users.controller.spec.ts (9.474 s)
コントローラーのテストも成功していることが確認できました。
これで、ユーザーを作成するAPIの実装が終わりました。
ユーザー情報を取得するAPIの実装
次に、ユーザー情報を取得するAPIを実装していきます。全てのユーザー情報を取得するAPIと、特定のユーザー情報を取得するAPIの2種類を作成します。
コントローラーにGetメソッドを追加
コントローラーにユーザー情報を取得するAPIのリクエストを追加するため、src/users/users.controller.ts
を編集します。
@Get() async findAll(): Promise<User[]> { return await this.usersService.findAll(); } @Get(':id') async findOne(@Param('id') id: number): Promise<User> { return this.usersService.findOne(+id); }
2つの@Get
デコレータを追加しています。引数がなければユーザー一覧を返し(findAll
)、引数でユーザーIDを指定すればそのユーザーの個別情報を返します(findOne
)。
サービスにfindAll()メソッドとfindOne()メソッドを追加
ユーザー情報を取得するメソッドを追加するため、src/users/users.service.ts
を編集します。
async findAll(): Promise<User[]> { return await this.userRepository.find().catch((e) => { throw new InternalServerErrorException( `[${e.message}]:ユーザーの取得に失敗しました。`, ); }); } async findOne(id: number): Promise<User> { return await this.userRepository .findOne({ where: { id: id }, }) .then((res) => { if (!res) { throw new NotFoundException(); } return res; }) }
ここでfindAll
がユーザー一覧を返し、findOne
が個別のユーザー情報を取得するメソッドです。
ユーザー情報を取得するAPIを実行
それではAPIを実行してみましょう。
# 全てのユーザーを取得するAPI $ curl -X GET http://localhost:3000/users [{"name":"hoge","id":1}] # 先ほど作成したユーザーが取得できている # 特定のユーザーを取得するAPI $ curl -X GET http://localhost:3000/users/1 {"name":"hoge","id":1} # 存在しないユーザーを取得しようとするとエラーになる $ curl -X GET http://localhost:3000/users/2 {"statusCode":404,"message":"Not Found"}
先ほど実装したユーザーを作成するAPIで作成したユーザーが取得できていることが確認できました。
これで、ユーザーの登録も取得も問題なく動作していることが確認できました。
サービスのテストを作成
最後にテストを作成していきます。
まず、src/users/users.service.spec.ts
を編集します。
describe('findAll()', () => { it('should return users', () => { const user: User = { id: 1, name: '太郎', }; jest.spyOn(service, 'findAll').mockImplementation(async () => { return [user]; }); expect(service.findAll()).resolves.toEqual([user]); }); it('should return empty array by Not found users', () => { const user: User[] = []; jest.spyOn(service, 'findAll').mockImplementation(async () => { return user; }); expect(service.findAll()).resolves.toEqual(user); }); }); describe('findOne()', () => { it('should return user', () => { const user: User = { id: 1, name: '太郎', }; jest.spyOn(service, 'findOne').mockImplementation(async () => { return user; }); expect(service.findOne(1)).resolves.toEqual(user); }); it('should return not found exception', () => { jest.spyOn(service, 'findOne').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); expect(service.findOne(2)).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); });
コントローラーのテストを作成
次に、src/users/users.controller.spec.ts
を編集します。
describe('findAll()', () => { it('should return users', () => { const user: User = { id: 1, name: '太郎', }; jest.spyOn(service, 'findAll').mockImplementation(async () => { return [user]; }); expect(controller.findAll()).resolves.toEqual([user]); }); it('should return empty array by Not found users', () => { const user: User[] = []; jest.spyOn(service, 'findAll').mockImplementation(async () => { return user; }); expect(controller.findAll()).resolves.toEqual(user); }); }); describe('findOne()', () => { it('should return user', () => { const user: User = { id: 1, name: '太郎', }; jest.spyOn(service, 'findOne').mockImplementation(async () => { return user; }); expect(controller.findOne(1)).resolves.toEqual(user); }); it('should return not found exception', () => { jest.spyOn(service, 'findOne').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); expect(controller.findOne(2)).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); });
テストを実行
テストを実行してみましょう。
$ npm run test PASS src/app.controller.spec.ts (7.641 s) PASS src/users/users.controller.spec.ts (11.238 s) PASS src/users/users.service.spec.ts (11.238 s) Test Suites: 3 passed, 3 total Tests: 11 passed, 11 total Snapshots: 0 total Time: 11.973 s, estimated 12 s
問題なく動作していることが確認できました。
ユーザーを更新・削除するAPIの実装
最後に、ユーザーを更新・削除するAPIを実装していきます。
コントローラーにPatch・Deleteメソッドを追加
HTTPリクエストを記述するため、src/users/users.controller.ts
を編集します。
@Patch(':id') async update( @Param('id') id: string, @Body() createUserDto: CreateUserDto, ): Promise<User> { return this.usersService.update(+id, createUserDto); } @Delete(':id') async remove(@Param('id') id: string): Promise<DeleteResult> { return this.usersService.remove(+id); }
update()
メソッドは@Patch
デコレータで、remove()
メソッドは@Delete
デコレータです。
サービスにupdate()メソッドとremove()メソッドを追加
更新と削除のメソッドを追加するため、src/users/users.service.ts
を編集します。
async update(id: number, createUserDto: UpdateUserDto): Promise<User> { const user = await this.userRepository.findOne({ where: { id: id } }); if (!user) { throw new NotFoundException(); } user.name = createUserDto.name; return await this.userRepository.save(user); } async remove(id: number): Promise<DeleteResult> { const user = await this.userRepository.findOne({ where: { id: id } }); if (!user) { throw new NotFoundException(); } return await this.userRepository.delete(user); }
ここではupdate()
メソッドとremove()
メソッドを追加しています。
APIを実行
それでは、APIを実行してみましょう。
# ユーザーを更新するAPI $ curl -X PATCH http://localhost:3000/users/1 -H "Content-Type: application/json" -d '{"name": "太郎"}' {"id":1,"name":"太郎"} # ユーザーを削除するAPI $ curl -X DELETE http://localhost:3000/users/1 {"raw":[],"affected":1}
ユーザを更新して削除できることが確認できました。
サービスのテストを作成
最後にテストを定義していきます。
まず、src/users/users.service.spec.ts
を編集します。
describe('update()', () => { it('should return update result user', () => { const dto: CreateUserDto = { name: '太郎2', }; const user: User = { id: 1, name: '太郎2', }; jest.spyOn(service, 'update').mockImplementation(async () => { return user; }); expect(service.update(1, dto)).resolves.toEqual(user); }); it('should return not found exception', () => { jest.spyOn(service, 'update').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); const dto: CreateUserDto = { name: '太郎2', }; expect(service.update(2, dto)).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); }); describe('remove()', () => { it('should return remove result', () => { const result: DeleteResult = { raw: [], affected: 1, }; jest.spyOn(service, 'remove').mockImplementation(async () => { return result; }); expect(service.remove(1)).resolves.toEqual(result); }); it('should return not found exception', () => { jest.spyOn(service, 'remove').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); expect(service.remove(2)).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); });
コントローラーのテストを作成
続いて、src/users/users.controller.spec.ts
を編集していきます。
describe('update()', () => { it('should return update result user', () => { const dto: CreateUserDto = { name: '太郎2', }; const user: User = { id: 1, name: '太郎2', }; jest.spyOn(service, 'update').mockImplementation(async () => { return user; }); expect(controller.update('1', dto)).resolves.toEqual(user); }); it('should return not found exception', () => { jest.spyOn(service, 'update').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); const dto: CreateUserDto = { name: '太郎2', }; expect(controller.update('2', dto)).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); }); describe('remove()', () => { it('should return remove result', () => { const result: DeleteResult = { raw: [], affected: 1, }; jest.spyOn(service, 'remove').mockImplementation(async () => { return result; }); expect(controller.remove('1')).resolves.toEqual(result); }); it('should return not found exception', () => { jest.spyOn(service, 'remove').mockRejectedValue({ statusCode: 404, message: 'Not Found', }); expect(controller.remove('2')).rejects.toEqual({ statusCode: 404, message: 'Not Found', }); }); });
テストの実行
テストを実行してみましょう。
$ npm run test PASS src/app.controller.spec.ts (6.911 s) PASS src/users/users.controller.spec.ts (9.366 s) PASS src/users/users.service.spec.ts (9.383 s) Test Suites: 3 passed, 3 total Tests: 15 passed, 15 total Snapshots: 0 total Time: 10.136 s, estimated 12 s Ran all test suites.
問題なく動作していることが確認できました。以上で、CRUDの実装は完了です。いかがでしたか? 全体的な流れのイメージが少しでもつかめたでしょうか?
今回は、CRUDの実装を行いましたが、実際の業務ではもっと複雑な処理を実装することになります。その際には、NestJSのドキュメントを参考にしてください。
また今回は割愛しましたが、Jestを使ったテスト作成で分からない部分があれば、Jestのドキュメントを参考にしてください。
まとめ
以上で、NestJSの基本的な使い方を紹介しました。NestJSは、Expressをベースにしているので、Expressの知識があれば、すぐに使いこなせると思います。今回触れなかった部分も多々ありますが、今後の参考にしていただければと思います。
興味を持った方は、ぜひ、NestJSを使ってみてください。一緒にNestJSを盛り上げていきましょう! ご興味を持っていただけた方は、ぜひ、NestJS Japan Users Groupへのご参加をお待ちしております。
過去のイベントアーカイブにも、NestJSに関する動画が載っていますので、ぜひご覧ください。
羽馬 直樹(HABA Naoki)Twitter: @NaokiHaba, GitHub: NaokiHaba
編集:中薗 昴
制作:はてな編集部