😎

NestJSに入門してみた

2023/05/08に公開

こんにちは!
フロントの方で使ってみたいライブラリがあったりして、自分用のツールの開発を行なっていたのですが、何となく NestJS も使ってみたくなりました。
ということで、NestJS に入門してみました!!

本記事では、今回 NestJS で開発をして学んだことをまとめていきます。

NestJS とは

NestJS は、TypeScript で構築された、バックエンド開発のための Node.js フレームワークです。Angular の影響を受けて開発されているようです。

https://docs.nestjs.com/

Controllers

Controllersは、ルーティングを記述し、クライアントからリクエストを受け取り、レスポンスを返す役割をします。
@Controller()デコレータを記述することで、Controllersとして定義することができます。

Providers

Providersでは、Serviceファイルを作成して、処理・ロジックを記述します。
Controllersがリクエストを受け取って、主なロジック部分はProvidersに任せてしまうのです。
@Injectable()デコレータを記述します。

Modules

NestJS では、modular architectureを採用しています。
機能ごとに1つのモジュールとしてまとめます。機能ごとのモジュールが集まって、1つの NestJS アプリケーションが構成されます。つまり、Modulesは、関連するルーティングやロジック等をまとめる役割を持つのです。
NestJS では、1つのアプリケーションには必ず1つのルートモジュールを持たなければなりません。ルートモジュールには、機能ごとに用意したモジュールをimportsの配列に入れる必要があります。

@Module()デコレータを記述することで、NestJS がアプリケーションの構造を整理するためのメタデータを提供します。providerscontrollersimportsexportsといったプロパティを持つオブジェクトを引数にとります。

基本的なところ

早速、実際に NestJS を使ってみたいと思います!
とりあえず、Hello World!を出してみます。

準備

まず、Nest CLI をインストールします。

npm install -g @nestjs/cli

Nest CLI を用いて、プロジェクトを作ります。project-name には、任意の名前を入れます。

nest new project-name

これで、開発の準備はできました。とりあえずsrc/に出来たファイルをいくつか見てみましょう。

app.module.ts

これが、ルートモジュールです。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [], // モジュールを要素として入れる配列
  controllers: [AppController], // ルーティングを記述したControllers
  providers: [AppService], // 処理を記述したProviders
})
export class AppModule {}

@Moduleデコレータに、ControllersProvidersが配列でまとめられているオブジェクトを渡しています。

app.controller.ts

AppControllerについて見てみます。これは、app.controller.tsに記述されています。
プロジェクト生成時にすでに1つルーティングが定義されています。
ここでは、GET /で、AppServiceクラスに記述されたgetHelloメソッドが実行されるようになっています。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    // AppServiceクラスに記述したgetHelloメソッドを実行
    return this.appService.getHello();
  }
}

app.service.ts

続いて、ロジックが記述されたAppServiceクラスを見てみます。
こちらもプロジェクト生成時にすでに1つメソッドが定義されています。
先ほど出てきた、getHelloメソッドが定義されています。このgetHelloメソッドはHello World!と返すようになっています。

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

main.ts

エントリーファイルも見てみます。
このmain.tsNestFactoryを使ってインスタンスを生成し、listenメソッドで起動します。ポートは引数で設定でき、デフォルトは3000です。

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

async function bootstrap() {
  // NestFactoryでインスタンスを生成
  const app = await NestFactory.create(AppModule);
  // ポート番号を引数で指定できる
  await app.listen(3000);
}
bootstrap();

起動すると...

それでは、npm run startして、http://localhost:3000/にアクセスしてみます。

無事、Hello World!できました!

@ってついてるやつ、何??〜デコレータ〜

これまで生成されたファイルをいくつか見てみましたが、ところどころ「@」がついた記述がありました。これは、「デコレータ」と呼ばれるものです。
デコレータとは、クラスやメソッド、プロパティ、アクセサ、パラメータに適用できる関数です。

メソッドやルーティングを追加してみる

最初に生成されたファイルを眺めているだけでは面白くないので、メソッドやルーティングを追加してみましょう。

Good Bye!を返すメソッドを作る

それでは、app.service.tsを編集していきます。
Good Bye!と返す、getGoodByeメソッドを追加しています。

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }

  // 追加
  getGoodBye(): string {
    return 'Good Bye!';
  }
}

ルーティングを追加

getGoodByeメソッドが定義できたので、ルーティングを追加します。
app.controller.tsを編集します。

追加された@Getデコレータを見てください。引数に文字列goodByeが渡されています。
これは、GET /goodByegetGoodByeメソッドが実行されるという実装になっています。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  // 追加
  @Get('goodBye')
  getGoodBye(): string {
    return this.appService.getGoodBye();
  }
}

表示を確認してみる

それでは、npm run startして、http://localhost:3000/goodByeにアクセスしてみます。

Good Bye!と表示されました!

API 実装に挑戦する

NestJS の使い方が少しわかってきたところで、フロントエンドから API を叩いてデータの取得や追加ができるようなサービスを想定して、バックエンドの実装を行なってみたいと思います!
任意の名前でディレクトリを作り、その中でメソッド・ルーティングを記述するファイルなど作成していきます。
データベースは、Amazon DynamoDB を使っていきます。DynamoDB を選定した理由としては、元々フロントエンドの方で DynamoDB への項目取得・追加を実装しようとして準備をしていたので、そのまま使うことにしました。DynamoDB はお仕事でも使用経験があったので、多少慣れていたからっていうのもあります。

DynamoDB のテーブルは以下の表のようになっている想定で進めていきます。

属性 パーティションキー/ソートキー
date(yyyy/MM/dd) String パーティションキー
timestamp Number ソートキー
weight Number

また、扱いやすくするため、DynamoDB ドキュメントクライアントを利用しています。

メソッドを作っていく

ここからは、Service ファイルにロジックを書いていきます。(dynamoDB.service.tsを用意しました。)

DynamoDB のインスタンスを作成する

import { Injectable } from '@nestjs/common';
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

@Injectable()
export class DynamoDBService {
  ddbDocClient: DynamoDBDocumentClient;

  // DynamoDBのインスタンス作成
  constructor() {
    const ddbClient: DynamoDBClient = new DynamoDBClient({
      region: process.env.AWS_REGION,
      credentials: fromCognitoIdentityPool({
        clientConfig: {
          region: process.env.CREDENTIAL_REGION,
        },
        identityPoolId: process.env.IDENTITY_POOL_ID,
      }),
    });

    this.ddbDocClient = DynamoDBDocumentClient.from(ddbClient);
  }
}

regionなどは.envに格納しています。

環境変数の使用について

認証関係の情報などは、.envに環境変数として格納しておきます。
NestJS では、.envに記述された環境変数を使用するためには、ConfigModuleが必要になります。これは、@nestjs/configというパッケージに含まれているので、別途インストールする必要があります。

npm install --save-dev @nestjs/config

この@nestjs/configは、内部でdotenvを使用していて、それで.envを読み込めるという仕組みだと考えられます。
次に、ルートモジュールapp.module.tsimportsに追加します。これで、他のモジュールでも.envから環境変数を読み込むことが可能になります。環境変数が必要なモジュールのimportsのみに追加することも可能です。

NestJS での環境変数の利用については、公式ドキュメントのConfigrationも参照してみて下さい。

項目の取得

ScanCommandを使用して、テーブルにある項目(データ)を全て取得してきます。
scanItemsメソッドを定義しています。
インスタンスを作成した時の認証情報と同じく、テーブル名は、環境変数にしています。

async scanItems(): Promise<ScanCommandOutput> {
  const param: ScanCommandInput = {
    TableName: process.env.DYNAMODB_TABLE_NAME,
  };

  try {
    const data: ScanCommandOutput = await this.ddbDocClient.send(
      new ScanCommand(param),
    );

    // ここで日付順になるよう並べ替え
    data.Items.sort((a, b) => {
      return a.timestamp - b.timestamp;
    });

    return data;
  } catch (err: unknown) {
    console.log('err', err);
  }
}

ScanCommandでは、ソートキーによる並べ替えが行われないので、ScanCommand実行後に項目(Items)を並べ替えます。日付順にしたいので、各項目の属性timestampを使用してsortメソッドを実行して並べ替えています。

このScanCommandは、項目以外にも、項目の総数を示すCountなどいくつかの情報をオブジェクトとして返してくれます。

項目を追加する

続いて、PutCommandを使用して、テーブルに項目を追加できるようにしていきます。
リクエストボディとして受け取ったオブジェクトを引数にとる、putItemメソッドを定義しています。

async putItem(requestBody: AddItemFormDataType): Promise<PutCommandOutput> {
  const params: PutCommandInput = {
    TableName: process.env.DYNAMODB_TABLE_NAME,
    Item: {
      date: requestBody.date,
      timestamp: requestBody.timestamp,
      weight: requestBody.weight
    },
  };

  try {
    const data: PutCommandOutput = await this.ddbDocClient.send(
      new PutCommand(params),
    );

    return data;
  } catch (err: unknown) {
    console.log('err', err);
  }
}

PutCommandの引数に渡すオブジェクトには、追加対象のTableNameは先ほどと同じく環境変数で指定、追加する項目(Item)は引数で受け取ったオブジェクトの中身を渡しています。

ルーティングを書いていく

必要なメソッドが定義できたので、ルーティングを書いていこうと思います。(dynamoDB.controller.tsを用意しました。)

import { PutCommandOutput, ScanCommandOutput } from '@aws-sdk/lib-dynamodb';
import { Body, Controller, Get, Post } from '@nestjs/common';
import { AddItemFormDataType } from '../types/AddItemFormDataType';
import { DynamoDBService } from './dynamoDB.service';

@Controller('dynamoDB')
export class DynamoDBController {
  constructor(private readonly dynamoDBService: DynamoDBService) {}

  // GET /dynamoDB/getItems
  @Get('getItems')
  getItems(): Promise<ScanCommandOutput> {
    return this.dynamoDBService.scanItems();
  }

  // POST /dynamoDB/addItem
  @Post('addItem')
  addItem(@Body() requestBody: AddItemFormDataType): Promise<PutCommandOutput> {
    return this.dynamoDBService.putItem(requestBody);
  }
}

@Controllerデコレータの引数に文字列を渡すことで、関連するルートをまとめることができます。

constructorで引数に、使用したいProviderを渡しています。

GET /dynamoDB/getItemsで、テーブルに入っている項目を取得するscanItemsメソッドが実行されます。返り値は、scanItemsメソッドの返り値です。@GETデコレータの引数に文字列を渡すことで、ルートを指定しています。
POST /dynamoDB/addItemで、テーブルに項目を追加するputItemメソッドが実行されます。こちらも、@POSTデコレータの引数に文字列を渡して、ルートを指定しています。

モジュールにまとめる

メソッドとルーティングが用意できました。
それでは、これらをモジュールにまとめたいと思います。(dynamoDB.module.tsを用意しました。)

import { Module } from '@nestjs/common';
import { DynamoDBController } from './dynamoDB.controller';
import { DynamoDBService } from './dynamoDB.service';

@Module({
  imports: [],
  controllers: [DynamoDBController],
  providers: [DynamoDBService],
})
export class DynamoDBModule {}

controllersには、先ほどルーティングを記述したDynamoDBControllerを、providersには、メソッドを記述したDynamoDBServiceを配列の要素として追加します。

ルートモジュールに追加

最後に、DynamoDB 操作関係のメソッドやルーティングをまとめたモジュールdynamoDB.module.tsをルートモジュールにimportします。

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DynamoDBModule } from './aws/dynamoDB.module';

@Module({
  imports: [ConfigModule.forRoot(), DynamoDBModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

あとは、フロントエンドから、API を叩くだけ!
...と思いましたが、フロントエンドから API 叩いたら、このままだと CORS エラーが出るので、main.tsを修正します。

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 追加する
  app.enableCors();
  await app.listen(9000);
}
bootstrap();

CORS を許可するenableCorsメソッドを追加します。ポートもフロントと被らないように 9000 に変更しておきます。
これで、本当に実装は完了です。

DynamoDB のテーブルに格納されている項目が取得できたり、DynamoDB のテーブルに項目を追加したりできます。

おわりに

今回は、NestJS に入門してみた経験についての記事でした!
Angular の経験はないですが、API 実装をやってみるっていうところまでは大きな問題なく進められたかなと思います。理解がまだまだ浅い部分も多々ありますが、とりあえず「NestJS に入門してみる!」ところまではできたので、自分としては満足です 😊

お読み下さり、ありがとうございました!

参考資料

NestJS 公式ドキュメント
A Complete Guide to TypeScript Decorators

GitHubで編集を提案

Discussion