NestJSに入門してみた
こんにちは!
フロントの方で使ってみたいライブラリがあったりして、自分用のツールの開発を行なっていたのですが、何となく NestJS も使ってみたくなりました。
ということで、NestJS に入門してみました!!
本記事では、今回 NestJS で開発をして学んだことをまとめていきます。
NestJS とは
NestJS は、TypeScript で構築された、バックエンド開発のための Node.js フレームワークです。Angular の影響を受けて開発されているようです。
Controllers
Controllers
は、ルーティングを記述し、クライアントからリクエストを受け取り、レスポンスを返す役割をします。
@Controller()
デコレータを記述することで、Controllers
として定義することができます。
Providers
Providers
では、Service
ファイルを作成して、処理・ロジックを記述します。
Controllers
がリクエストを受け取って、主なロジック部分はProviders
に任せてしまうのです。
@Injectable()
デコレータを記述します。
Modules
NestJS では、modular architecture
を採用しています。
機能ごとに1つのモジュールとしてまとめます。機能ごとのモジュールが集まって、1つの NestJS アプリケーションが構成されます。つまり、Modules
は、関連するルーティングやロジック等をまとめる役割を持つのです。
NestJS では、1つのアプリケーションには必ず1つのルートモジュールを持たなければなりません。ルートモジュールには、機能ごとに用意したモジュールをimports
の配列に入れる必要があります。
@Module()
デコレータを記述することで、NestJS がアプリケーションの構造を整理するためのメタデータを提供します。providers
やcontrollers
、imports
、exports
といったプロパティを持つオブジェクトを引数にとります。
基本的なところ
早速、実際に 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
デコレータに、Controllers
やProviders
が配列でまとめられているオブジェクトを渡しています。
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.ts
でNestFactory
を使ってインスタンスを生成し、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 /goodBye
でgetGoodBye
メソッドが実行されるという実装になっています。
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.ts
のimports
に追加します。これで、他のモジュールでも.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 に入門してみる!」ところまではできたので、自分としては満足です 😊
お読み下さり、ありがとうございました!
Discussion