9wandinavian
nestjs-thumbnail.png

NestJsの基本

NestJSのアーキテクチャとDIの方法をまとめる。
どうでもいいけどNestJSのロゴと「旬」の字は似ている。「勺」もそこそこ。

セットアップ

次のコマンドを実行。

$ npm i -g @nestjs/cli
$ nest new <プロジェクト名>

nest new <プロジェクト名>した後に、使用するパッケージマネージャーを聞かれたらyarnを選択する(npm/yarn/pnpmから好みで)

プロジェクトが作成されると、srcフォルダ以下が次のようになる。

src
┣app.controller.spec.ts // ユニットテスト用
┣app.controller.ts // コントローラ。パスに対するリクエスト→コントローラの対応づけ
┣app.module.ts // アプリケーション全体のフィーチャーモジュールを束ねるボス
┣app.service.ts // ビジネスロジック担当main.ts // アプリケーションのエントリポイント

[余談]tsconfig.jsonのcompilerOptions

"strict": true"noImplicitAny": true に設定しておいた方がいい

{
  "compilerOptions": {
    略
    "noImplicitAny": true"strict": true
  }
}

この時点でnode_modulesがなかったらyarnを実行してパッケージインストール。

NestJSの基本3要素

  • Controller
  • Service
  • Module

基本的には、これら3つを単位として1機能ができるイメージ
e.g. ユーザ関連の機能では、
UserController, UserService, UserModule

全体像

NestJSのアーキテクチャ.png
※featureモジュールを作成後は、ルートモジュールに登録すること。
例えばUserModuleを作成した場合は、次のようにルートモジュールへ登録する。

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

@Module({
  imports: [UserModule], // ここ
})
export class AppModule {}

※cliでモジュール作成$ nest g mo userした場合は自動でルートモジュールへ登録される。

Module

Moduleは、コントローラーやサービスをまとめ、アプリケーション内で利用できるようにNestJSに登録する役割を持つ。
cliで次のようにして作成可能("cats"という "mo"duleを "g"enerateする)

$ nest g mo cats

次のコードはfeatureモジュールを想定した説明用サンプル。
@Module()デコレータをつけることにより、そのクラスをモジュールとして定義できる。
この@Module()デコレータ内にオブジェクト形式でプロパティを記述することで、コントローラーやサービスを登録する。

// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { PrismaModule } from '.......';
import { PrismaService } from '.......';
import { JwtStrategy } from '.......';
import { JwtAuthGuard } from '.......';

@Module({
  imports: [PrismaModule], // 他のモジュールの機能を利用する
  controllers: [CatsController],
  providers: [CatsService], // DIして使う
  exports: [JwtStrategy, JwtAuthGuard, PrismaService], // 他のモジュールでもimport可能になる

})
export class CatsModule {}

🔍@Moduleデコレータ内のプロパティを簡単に説明
imports: []:そのモジュール内で使用したい外部モジュールを追加する。データベースモジュールや認証モジュールなど。

controllers: []@Controllerデコレータが付与されたクラスを追加する。

providers: []@Injectableデコレータが付与されたクラス(多くはServiceクラス)を追加することで、DIを可能とする。

exports: []:他のモジュールでも広く利用するような機能を追加する。

くどいけどfeatureモジュールを作ったなら、 ルートモジュールへの登録を忘れずに。

// app.module.ts
@Module({
  imports: [CatsModule],
})

Controller

Controllerは、クライアントからのリクエストを受けてクライアントにレスポンスを返す。ルーティング処理担当。
cliでは次のようにして作成可能("products"という "co"ntrollerを "g"enerateする)

$ nest g co products

次のコードは、featureモジュールを想定した説明用サンプル。
@Controller()デコレータをつけることで、そのクラスをコントローラとして定義できる。
また、この@Controller()デコレータの引数に('パス名')を記述することで、エンドポイントのパスを指定できる。下のソースは /products への各種リクエストということになる。

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}
  // GET /products 
  @Get()
  async findAll(): Promise<Products[]> {
    return await this.productsService.findAll();
  }

  // GET /products/81ba48e7-54a1-4bb3-9122-14e79471f174
  @Get(':id')
  async findById(@Param('id', ParseUUIDPipe) id: string): Promise<Products> {
    return await this.productsService.findById(id);
  }

  // POST /products
  @Post()
  // 略

  // PATCH /products/81ba48e7-54a1-4bb3-9122-14e79471f174
  @Patch(':id')
  // 略

  // DELETE /products/81ba48e7-54a1-4bb3-9122-14e79471f174
  @Delete(':id')
  // 略
}

/products からさらにパス階層を掘りたい場合は、各HTTPメソッドハンドラのデコレータにも引数を渡せば可能。→@Get(':id')が参考になる。
メソッドハンドラの中身では、Serviceクラスのメソッドを呼んでビジネスロジックを実行させる。

Service

ビジネスロジックを定義する。
@Injectableデコレータをつけることで、そのクラスをDI用のクラスとして定義できる。

@Injectable()
export class ProductsService {
  constructor(private readonly productRepository: ProductRepository) {}

  async findAll(): Promise<Product[]> {
    return await this.productRepository.find();
  }

  async findById(id: string): Promise<Product> {
    const foundItem = await this.productRepository.findOne(id);
    if (!foundItem) {
      throw new NotFoundException();
    }
    return foundProduct;
  }
}

次にこの@InjectableデコレータがついたクラスをModuleのprovidersに追加することで、DIの準備が整う。

@Module({
  controllers: [ProductsController],
  providers: [ProductsService]
})

最後にControllerクラスのconstructorの引数にDI用のServiceクラスを渡し、construction DIを行う。

@Injectable()
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}
  …
}

NestJSにおけるDIとは...

以下はプロジェクト作成時に自動生成されたAppControllerがAppServiceのgetHelloメソッドを呼び出している例
AppService.png
AppController.png

特徴として、

  • Controller内では、AppServiceをインスタンス化していない
  • 代わりに、Controllerがインスタンス化される際、constructorに記述されたAppService(appService)も自動的にインスタンス化される。これはNestJSのランタイムシステムが内包している(Inversion of Controlコンテナという 仕組み? 手法?)にやらせることで実現できているらしいがよくわからん。

上記を経て、AppControllerにAppServiceのgetHelloメソッドがInjectionされる。

以上