Open8

NestJSのTips

ぴよぴよ

リクエストとかレスポンスをどうするか

  • リクエストはDtoを作って定義する
    • ここにclass validatorもつける
    • class validatorはnestもできるので、入れ子のdictの中もチェックしたいときは@ValidateNested()をつけるとよい
  • レスポンス
    • このときIntersectionTypeなどを使って既存のEntityや型を組み合わせたりすると楽

このへんの詳しい書き方はあとで記事をかく。

ぴよぴよ

JWTまわり

チュートリアルにもあるJWTの署名は秘密鍵がローカルにある必要がある。普通KMSとかで鍵自体はおかないようにすることが多いので、チュートリアルのようにPassportを使ってJWTを実装するときは気を付ける必要がある。
とりあえずプログラマの目にとまるよってだけなのでSecret Managerあたりで鍵を隠す+鍵のローテーションをするという運用がよさそう。

テスト

AuthGuardを使ったとき、access_tokenなんていちいちテスト中にわからないので、モックする。
stackoverflowを参考に、

beforeEach(async () => {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  })
    .overrideGuard(Guard1)
    .useValue({ canActivate: () => true })
    .overrideGuard(Guard2)
    .useValue({ canActivate: () => true })
    .compile();

  app = moduleFixture.createNestApplication();
  await app.init();
});

こんな感じでoverrideGuardしてuseValueで値を指定すればOK。ちなみにNestJSにはbeforeAllかbegoreEachにmoduleに書いた依存関係を書く落とし穴があります。

bcryptでパスワードを守る

  • saltRoundsは大きすぎず小さすぎない値である必要がある。リポジトリのサンプルの10が無難そう
  • bcryptはbcyptのリポジトリにもあるようにasyncでハッシュにすることを推奨している
//hash化するとき
const password_hash = await bcrypt.hash(password, 10);//10はsaltRounds
//compareするとき
const result = await bcrypt.compare(password, password_hash);
ぴよぴよ

import

  • なぜか相対パスでやらないとエラーになることがある
ぴよぴよ

テストのモック

  • Unit TestだとspyOnでモックできる
  • e2eテストの場合、createTestingModuleを使うはず。なので依存するserviceについてprovideとuserValueを使ってモックする
    • インスタンス化をテストコード内でしていないとspyOnが使えないので
    • 当たり前だけどe2eテストはHTTP通信をしたことにするので、まああんまりメソッドのモックとかはしないんだろうなという気持ち
ぴよぴよ

NestJSの入出力のバリデーションとかにおすすめのライブラリ

Interceptorの使い所

validation pipeはclass-validatorに使うのかなとすぐ理解できたのですが、intercepterだけ使い所がわからなかったのでメモ。
class transformerとかを使うときに使う。
例えば

  • あるjsonの中で、名前つけかえたい
  • passwordだけ抜きたい
    みたいなのをアノテーションひとつでできる感じです。

使い方

同じような有名どころライブラリにclass validatorがあるのですが、これとほぼ導入方法は同じです。main.tsに以下をかきます。

//class-validatorを使うときに
nestApp.useGlobalPipes(new ValidationPipe());
//class-transformerを使うときに
nestApp.useGlobalInterceptors(
    new ClassSerializerInterceptor(nestApp.get(Reflector)),
  );

あとはリポジトリにある例を見てentityなりDTOなりのクラスにアノテーションをつけるだけ。
この2つのライブラリはNestJS使うときは入れておくとよさそう。

ぴよぴよ

他のServiceを使う時のチェックポイント

  • moduleのimportに相手のmoduleをimportしているか
  • importしたうえでprovidersに使う相手のserviceを書いているか
  • 自分のserviceのconstructorに相手のserviceを書いているか
  • testをしている場合、testのmoduleにも上記を反映しているか

わりとこのへんつまってdependenciesエラーが出て死ぬのでメモ

ぴよぴよ

Nestでつくるときのメモ

controller

DTO

  • req / resの値を入れておく
import {ApiProperty} from '@nestjs/swagger';
import {IsString* from 'class-validator';
import {HogeModel} from './hoge.model';

export class GetHogeRequestDto{
  @ApiProperty({ type: String, description: 'test', example: 'てすとです'})
  @IsString()
  test: string;
}

// Modelに一部追加してレスポンスを返す場合の例
export class GetHogeResponseDto extends HogeModel{
  // こっちはクライアントからのリクエストではないのでclass-validatorはいらない
  @ApiProperty({ type: [String], description: 'テスト一覧', example: ['テスト1', 'テスト2']})
  test_list: string[]:
}

controller

import {Body, Controller, HttpCode, HttpException, Post, UseGuards}
import {ApiBody, ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger';
import {JwtAuthGuard} from '../auth/hoge.jwt.stragegy'; //jwtとかのstrategyをt雨くる
import {HogeModel} from '../models/hoge.model';
import {HugaService} from './huga.service';
import * as HogeDTO from './dto/hoge.dto';

@Controller('huga')
@ApiTags('huga')
export class HugaController{
  constructor(private readonly hugaService: HugaService){}
  @Post('/')
  @HttpCode(200)
  @ApiOperation({summary: 'hogeを取得'})
  @ApiResponse({type: HogeDTO.GetHogeResponseDto, status: 200, description: 'hoge'})
  @ApiBody({type: HogeDTO.GetHogeRequestDto})
  @UseGuards(JwtAuthGuard)
async get(@Body() body:  HogeDTO.GetHogeRequestDto): Promise<HogeDTO.GetHogeResponseDto>{
  const hoge = await this.hugaService.get();
  return {
    ...service,
    test_list: ['test1']
  }
}
}

module

import {Module} from '@nestjs/common';
import {HugaService} from './huga.service';
import {HugaController} from './huga.controller';

@Module({
  providers: [HugaService],
  controllers: [HugaController],
})
export class HugaModule {}

service

import {HttpExeption, HttpStatus, Injectable} from '@nestks/common';
import {HogeModel, HogeStore} from './models/hoge.model';

@injectable()
export class HugaService{
  async get(); Promise<>{
  const hoge = await hogeStore.scan().execFetchAll();
  if(hoge?.length === 0){
    throw new HttpException("hoge is not exists", HttpStatus.NOT_FOUND);
  }
  return hoge;
}
}

DB接続まわり

  • DynamoDBだったのでDBとの接続クラスを作る(dynamo-easyなどのラッパーを使用)
  • modelがテーブルごとのクラスで、一応全部をまとめるbasemodelを空のクラスでつくっておく(あとで型定義に使うかも)
  • swaggerの定義をかくとDTOを作るときに楽(だけど、DTOで受け取る値を絞るような運用のときはやらないほうがいい。例えばpasswordなどの値を入れたくないとか)

hoge.model.ts

import {ApiProperty} from '@nestjs/swagger';
import {DynamoStore, Model, PartitionKey} from '@shiftcoders/dynamo-easy';
import {BaseModel} from './base.model';

@Mode({tableName: `Hoge_${process.env.STAGE}`}) //複数ステージで運用する
export class HogeModel extends BaseModel{
  @PartitionKey()
  @ApiProperty({ type: String, description: 'hogeid', example: 'id1'})
  id: string;

  @ApiProperty({ type: String, description: 'name', example: 'なまえ'})
  name: string;
}

export const HogeStore = new DynamoStore(HogeModel);
ぴよぴよ

あとでかく

  • enablecors
  • env使う(configmodule)
  • globalpipes
  • serverlessで動かす