Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

NestJSにざっくり入門してみた(part1)

目次

NestJSに入門した理由

TypeScriptでバックエンドを書いてみたいなと思ったのと、元々入る予定の会社がNestJSを使っていたので、入門しました。しかし、結局NestJSを使わない別の会社に入ることを決めたので、これまで学んだことだけブログにまとめようと思います。

NestJSとは

NestJSとは、Node.js上で動作するオープンソースのバックエンドフレームワークのことです。

NestJSの特徴

  • NestJS自体がTSで作られているので、TypeScriptと相性が良いです。
  • Expressをコアにして作られているので、ExpressでできることはNestJSでもできます。
  • Angularにインスパイアされています。(ファイルの命名規則とか)

NestJSのメリット

  • TypeScriptを使うので、型の恩恵を得ることができます。
  • Expressの機能やライブラリを使えます。
  • Nest CLIというコマンドラインインターフェースが用意されているので、プロジェクトの作成やファイルのテンプレートをコマンドによって生成できます。
  • テストフレームワークが標準で用意されています。
  • NestJSは拡張性が高いです。RDB, NoSQL, セキュリティやGraphQL, WebSocketなど、必要に応じて拡張できます。

NestJSのデメリット

  • Railsに比べて日本でのユーザー数が少なく、ドキュメントが少ないです。
  • 公式ドキュメントが日本語に未対応です。

Nest CLIとは

Nest CLIとは、NestJSが用意しているコマンドラインインターフェースです。プロジェクトの作成やファイルのテンプレートをコマンドによって生成できます。

↓ 新規プロジェクトの作成

nest new プロジェクト名
// 既にプロジェクトのディレクトリを作っているなら、.でも良い
nest new .

↓ コントローラの作成

nest g controller コントローラ名

Nest CLIを使うメリット

Nest CLIを使うことで、TypeScriptやPrettier, ESLintの設定、ディレクトリ構成やファイルの生成などを簡単に行えます。

NestJSを利用する方法

NestJSを利用する方法は2つ存在します。

方法1: npmでNest CLIをインストールする

方法2: GitHubにあるNestJSのスタータープロジェクトをクローンする

今回は方法1を採用しました。以下のコマンドを実行することで、Nest CLIをインストールできます。

npm install -g @nestjs/cli

srcディレクトリに格納されているファイルについて

新規プロジェクトを作成すると、srcディレクトリには以下のファイルが格納されています。

Image from Gyazo

【ファイルについての詳細な説明】

  • コントローラ、モジュール、サービスがNestJSの基本要素になっています。
  • specがついたファイルはtestファイルです。

main.tsの役割について

main.tsはプロジェクトのエンドポイントになります。NestJSの起動コマンドを実行すると、まずmain.tsが実行されます。そして順番に必要なファイルが実行され、すべての準備が整うと、app.listenの引数に渡されたポート番号でクライアントからのリクエストを待ちます。

↓ main.ts

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();

開発を進めるに当たっての準備

app.controller.spec.ts, app,controller.ts, app.service.tsは使用しないので、消します。その後、app.module.ts内のそれらに関する記述を消します。

NestJSの基本アーキテクチャ

↓ 画像引用(NestJSにざっくり入門してみる)

NestJSのアーキテクチャ

アーキテクチャについての詳細な説明】

  • コントローラ、モジュール、サービスがNestJSの基本要素になっています。原則これら3つが揃って1つの機能を作っていきます。
  • アプリケーションのエンドポイントは main.tsになります。main.tsにはルートモジュールであるapp.module.tsを登録します。ルートモジュールは、開発した機能を集約して、アプリケーションに登録する役割を持ちます。すなわち、ルートモジュールに登録された機能のみがアプリケーションで利用できます。ルートモジュールに登録しないと、機能を使えません。
  • ルートモジュールに機能を登録したい場合、ルートモジュールにその機能を表すフィーチャーモジュールを登録します。フィーチャーモジュールの名前は、開発する機能名にするそうです(曖昧)。
  • フィーチャーモジュールには、フィーチャーサービスとフィーチャーコントローラを登録します。

↓ items.module.ts内に書かれたItemsModule(フィーチャーモジュール)

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

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

NestJSの基本的な開発の流れ

  1. フィーチャーサービスとフィーチャーコントローラを実装します。
  2. それらをフィーチャーモジュールに登録します。
  3. さらにそれをルートモジュールに登録します。

アプリケーションの機能が増えると、フィーチャーモジュール、フィーチャーサービス、フィーチャーコントローラの3点セットが複数できるようになります。フィーチャーモジュールのファイルをNest CLIのコマンドで生成した場合、フィーチャーモジュールをルートモジュールに自動で登録してくれます。同様に、サービスとコントローラのファイルをコマンドで生成した場合、それらをフィーチャーモジュールに自動で登録してくれます。

NestJSにおけるモジュールの役割

NestJSにおけるモジュールの役割は、関連するコントローラやサービスなどをまとめ、アプリケーションとして利用できるようにNestJSに登録することです。NestJSアプリケーションには、必ず1つ以上のルートモジュールと、0個以上のフィーチャーモジュールが必要になります。

モジュールの定義方法

モジュールを定義するには、クラス定義の前に、@moduleデコレータをつけます(@は人によって発音したり発音しなかったりする)。

↓ items.module.ts内に書かれたItemsModule(フィーチャーモジュール)

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

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

↓ app.module.ts内に書かれたAppModule(ルートモジュール)

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

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

この@moduleデコレータの中にオブジェクトの形式で必要なプロパティを記述することで、コントローラやサービスの登録ができます。

モジュールデコレータに定義できるプロパティ

モジュールデコレータには、以下の4つのプロパティを定義できます。

  1. providers
    providersには、@Injectableデコレータがついたクラスを記述します。 @Injectableデコレータがついたクラスを定義することで、Dependencyインジェクション(DI)ができるようになります。@Injectableデコレータをつける代表格は、サービスクラスです。つまり、providersには主にフィーチャーモジュールに登録したいサービスを書きます。
  2. controllers
    controllersには、@Controllerデコレータがついたクラスを記述します。つまり、controllersには、フィーチャーモジュールに登録したいコントローラを書きます。
  3. imports
    importsには、モジュール内部で利用したい外部モジュールを記述します。
  4. exports
    providers or importsに記述したもののうち、外部のモジュールでも利用したいものを記述します。他のプロパティに比べて、あまり使う場面はないですが、認証関連の機能など、複数のモジュールで横断して使いたい場合に使います。

モジュールを簡単に作る方法

新しいファイルを1から作っても良いですが、Nest CLIのコマンドを使うと、モジュールを定義するのに必要な記述を書いた状態でファイルを生成します。そして、そのファイル内に記述したモジュールをルートモジュールに(app.module.ts)自動で登録します。

↓ モジュール生成のコマンド

// このコマンドを実行すると、モジュール名のディレクトリとその配下にモジュール名.module.tsファイルが作られます。
// app.module.tsのimportに作成したモジュールが記述されている場合、app.module.tsにモジュールが登録されたことになります。
nest g module モジュール名

Controllerとは

コントローラは、クライアントからのリクエストを受け付けて、レスポンスを返す役割を持ちます。 NestJSでは、Railsのようにコントローラとルーティング機能が分離されているわけではなくて、コントローラがルーティング機能も担っています。

【コントローラの詳細な説明】

  • 特定のパスとコントローラが紐付けられます。(/usersとUsersController)。パスとコントローラの紐付けは、@Controlelrデコレータの引数にパス名を渡します。
  • HTTPメソッドデコレータとパスを指定したメソッド(ハンドラと呼ばれる)をコントローラ内に定義します。
  • メソッドをハンドラとして動作させたい場合、そのメソッドにHTTPメソッドハンドラをつける必要があります。メソッドをgetリクエストで実行させたい場合、@Getデコレータをつけます。ハンドラメソッド独自のパスを設定する場合、HTTPメソッド デコレータの中に文字列で記述します。

↓ items.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseUUIDPipe,
  Patch,
  Post,
} from '@nestjs/common';
import { ItemsService } from './items.service';
import { Item } from './item.model';
import { CreateItemDto } from './dto/create-item.dto';

@Controller('items')
export class ItemsController {
  // この1行を追加するだけで、NestJSがサービスクラスをインスタンス化し、変数に代入してくれます。
  // 変数はitemsServiceのこと
  constructor(private readonly itemsService: ItemsService) {}

  // GETメソッドのハンドラを作成する
  @Get()
  findAll(): Item[] {
    return this.itemsService.findAll();
  }

  // idをパスとして受け取る方法で実装する
  // /items/uuid
  // uuidの部分を可変にしたいなら、引数の前にコロンを入れる
  // ハンドラーメソッド独自のパスを設定する場合、このHTTPメソッドデコレータの中に文字列で記述します。
  @Get(':id')
  findById(@Param('id', ParseUUIDPipe) id: string): Item {
    return this.itemsService.findById(id);
  }

  // POSTメソッドのハンドラを作成する
  @Post()
  create(@Body() createItemDto: CreateItemDto): Item {
    return this.itemsService.create(createItemDto);
  }

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

  @Delete(':id')
  delete(@Param('id', ParseUUIDPipe) id: string): void {
    this.itemsService.delete(id);
  }
}

コントローラの定義方法

コントローラを定義するには、クラス定義の前に@Controllerというデコレータをつけます。その後、メソッド(ハンドラ)を定義して、そのメソッドにHTTPメソッドデコレータをつけます。

コントローラの作り方

以下のコマンドでコントローラを簡単に作成できます。

nest g controller コントローラ名

上のコマンドを実行すると、コントローラファイルとそれに対応するテストファイルが作成されます。テストファイルを作りたくない場合、—no-specオプションをつけます。上のコマンドで作ったコントローラは、フィチャーモジュールに自動で紐づけられます。

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

(注) コントローラ名はフィーチャーモジュールの名前と統一します。

デコレータとは

デコレーターとはクラス宣言やメソッドにつけることができるもので、デコレーターがつけられたクラスやメソッドに特別な機能を与えることができます。既存のクラスやメソッドにデコレーションする(=追加機能を入れる)イメージです。

NestJSで作成したアプリケーションを起動

以下のコマンドでNestJSで作成したアプリケーションを起動できます。

// :devはオプション
// :devをつけることで、コードの修正を自動的にアプリケーションに反映できる
npm run start:dev

Serviceとは

Serviceとは、実現したい機能、すなわち、ビジネスロジックを定義するためのものです。

【Serviceの詳細な説明】

  • コントローラからサービスを呼び出すことで、ユーザーからのリクエストを受け付け、具体的な処理をするといったユースケースを実現できます。
  • サービスに定義する内容は、コントローラに定義しても動作します。つまり、コントローラにビジネスロジックを書いてもプログラムは動作します。
  • サービスが必要な理由は、NestJSにおけるコントローラの役割はルーティングであるためです。本来の役割を超えた機能を実装していると、いずれプログラムは崩壊します。コントローラはルーティング、サービスはビジネスロジックと役割を分担することで、保守性と拡張性が高い設計にすることができます。

Dependency Injection (DI)とは

DIを直訳すると依存性の注入。DIとは、依存関係のあるオブジェクトを外部から渡すことです。コントローラはサービスを利用します。つまり、サービスを作成しない場合、コントローラを実行できません。このように別のプログラムがないと動作できない状態を「依存している」と言います。

サービスはクラスなので、どこかでインスタンス化しないと使えません。コントローラの中でサービスのインスタンスを作る場合、サービスとコントローラの依存度を高めてしまうので、推奨できません。本番用のサービスクラスとテスト用のサービスクラスがある場合、テストと本番で運用が煩雑になり、コードを書き換えないといけないので、バグのリスクが高まります。この場合、コントローラの外部でサービスのインスタンスを作成して、それをコントローラに渡すのが一般的です。このような外部から依存オブジェクトを渡すパターンのことをDependency Injectionと呼ばれています。

DIのメリット

DIのメリットは、依存元のプログラムを書き換えることなく、 依存先の切り替えを行えるところです。

DIコンテナとは

DIコンテナとは、DIを簡単に行える仕組みのことです。

Serviceの定義方法

サービスを定義するには、class定義の前に@Injectable()デコレータををつけます(@Seviceではない)。その後、ビジネスロジックを実現するメソッドを作成します。

サービスの作り方

以下のコマンドを実行すると、サービス用のファイルを作成してくれて、かつモジュールにサービスを登録してくれます。

nest g service サービス名 --no-spec

↓ items.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { ItemStatus } from './item-status.enum';
import { Item } from './item.model';
import { CreateItemDto } from './dto/create-item.dto';
import { v4 as uuid } from 'uuid';

@Injectable()
export class ItemsService {
  // 商品を保存するための配列変数を定義する
  // これはインスタンスに定義するフィールド
  private items: Item[] = [];

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

  findById(id: string): Item {
    const found = this.items.find((item) => item.id === id);
    if (found) {
      return found;
    } else {
      throw new NotFoundException();
    }
  }

  create(createItemDto: CreateItemDto): Item {
    const item = {
      id: uuid(),
      ...createItemDto,
      status: ItemStatus.ON_SALE,
    };
    this.items.push(item);
    return item;
  }

  // 商品が売れた場合に、ステータスを更新するAPIを実装する
  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);
  }
}

作成したサービスをコントローラで利用する

作成したサービスをコントローラから利用するには、2つのステップが必要です。

  1. DIのための設定をします。フィーチャーモジュールのprovidersにServiceを登録します。これだけで、NestJSが自動的にDIしてくれます。
  2. ControllerのコンストラクタでServiceを引数に設定します。

この2ステップのみでコントローラからサービスを利用できます。
DIを使いこなせると保守性や拡張性の高いプログラムが書けるようになります。

NestJSにおけるモデル

NestJSにおけるモデルとは、型定義のことです。手動でmodule名(単数形).model.tsを作成します。モデルはコントローラ内のハンドラの戻り値の型などに使います。他にも利用する場面はありそうですが、現時点では思いつかないので、気づきがあったら追記します。

↓ item.model.ts

import { ItemStatus } from './item-status.enum';

export type Item = {
  id: string;
  name: string;
  price: number;
  description: string;
  status: ItemStatus;
};

↓ items.controller.ts

import { Body, Controller, Get, Post } from '@nestjs/common';
import { ItemsService } from './items.service';
import { ItemStatus } from './item-status.enum';
import { Item } from './item.model';

@Controller('items')
export class ItemsController {
  // この1行を追加するだけで、NestJSがサービスクラスをインスタンス化し、変数に代入してくれます。
  // 変数はitemsServiceのこと
  constructor(private readonly itemsService: ItemsService) {}
  // 省略

  // POSTメソッドのハンドラを作成する
  // ハンドラの引数に@Bodyを使用することで、リクエストボディに格納された値を取得できます。
  // @Bodyの引数には、リクエストボディのキーを指定します。キーに対応する値がid変数に格納されます。 
  @Post()
  create(
    @Body('id') id: string,
    @Body('name') name: string,
    @Body('price') price: number,
    @Body('description') description: string,
  ): Item {
    const item = {
      id,
      name,
      price,
      description,
      status: ItemStatus.ON_SALE,
    };
    return this.itemsService.create(item);
  }
}

リクエスト時に渡すパラメータを受け取る2つの方法

リクエスト時に渡すパラメータを受け取る方法は、2つあります。

方法1: リクエストボディに値を入れます。その値を@Bodyデコレータで取得します。

方法2: パスに値を入れます。その値を@Paramsデコレータで取得します。

終わり

Railsの経験があったので、割とつまづくことなく進めることができました。Railsに比べると開発者体験は良さそうだが、仕組みが直感的ではないなと感じたので、気づきがあったら追記していこうと思います。

参考記事 & 動画

NestJSのススメ ~Expressを超えてゆけ~ - Qiita

NestJSにざっくり入門してみる

NestJSでよく見る@Decoratorって何なのかわからなかったからサクッと試してみた - ハイパーマッスルエンジニア

もう怖くないTypeScriptのDecorator機能

NestJS入門 TypeScriptではじめるサーバーサイド開発 | Udemy