Open17

[Scrap] NestJS 勉強メモ

連休で時間取れるので、オンラインコースをやってみることにする。$85 高いかもと思ったが、公式が出してるやつだし、英語分かりやすいしポチった。

https://learn.nestjs.com/
  • SQL (PostgreSQL) パートと NoSQL (mongoDB) パートの二段構成
    • SQL パート終わった後にブランチ生やして NoSQL パートをスタートするとのこと

まだ進捗10%くらいだけど、

  • 英語聞き取りやすい
  • 一回のレッスンが 1-3分とか長くても5分とかで短めで、中断しながら進めやすい
  • 楽しい

Controller について

Controller での parameter の取得の仕方

Param デコレータで抽出して取得する

@Controller('coffees')
export class CoffeesController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns #${id} coffee`;
  }
}

params からでも取得できる

@Controller('coffees')
export class CoffeesController {
  @Get(':id')
  findOne(@Param() params) {
    return `This action returns #${params.id} coffee`;
  }
}

Controller での body の取得の仕方

Body デコレータで取得する

import { Body, Controller, Get, Param, Post } from "@nestjs/common";

@Controller('coffees')
export class CoffeesController {
  @Post()
  create(@Body() body) {
    return body;
  }
}

個別で値を取り出す

@Get の時と同じで property をデコレータで指定する。

import { Body, Controller, Get, Param, Post } from '@nestjs/common';

@Controller('coffees')
export class CoffeesController {
  @Post()
  create(@Body('name') name) {
    return name;
  }
}

Status コードの返し方

使い分けや注意点も理解しきれていないが、以下の二つの書き方があるっぽい。

response オブジェクトで呼び出す

プラットフォームごとの挙動に注意が必要らしいが、全く分からん。

@Controller('coffees')
export class CoffeesController {
  @Get()
  findAll(@Res() response) {
    response.status(200).send('This action returns all coffees');
  }
}

メソッドのデコレータを使う

Response Code を返し分けたい時は使えなさそうだけど...
API を外部に公開していて duplicated になった時とかに、同じステータスコードを返すとかかな。

@Controller('coffees')
export class CoffeesController {
  @Post()
  @HttpCode(HttpStatus.GONE)
  create(@Body('name') name) {
    return `The coffee name ${name} is created.`;
  }
}

更新処理

Patchを使用する。Param, Body どちらも受け取って更新に使用する。

  @Patch(':id')
  update(@Param('id') id: string, @Body() body) {
    return `This action updates #${id} coffee.`;
  }

削除処理

Delete を使用する。

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes #${id} coffee`;
  }

HTTP Request method がそのまま使えるっぽい。

Service を定義する

実際の取得ロジックなどユースケースっぽいものを記述しておいて、Controller に Inject する。
(分けて書いてるのは、おそらく Controller 以外でも使えるのだろう)

こういう書き方をフレームワークに強制されるのは個人的には好き。(見た目が読みづらくなければ、という前提ありきだけど)

import { Injectable } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';

@Injectable()
export class CoffeesService {
  private coffees: Coffee[] = [
    {
      id: 1,
      name: 'Shipwreck Roast',
      brand: 'Buddy Brew',
      flavors: ['chocolate', 'vanilla'],
    },
  ];

  findAll() {
    return this.coffees;
  }
}

Controller 側では以下のように呼び出す。

@Controller('coffees')
export class CoffeesController {
  constructor(private readonly coffeeService: CoffeesService) {
  }

  @Get()
  findAll() {
    return this.coffeeService.findAll();
  }
}

video で service 側で HTTPException 呼び出してた。まじか、と思ったけどそんなものなのかな。

エラーコードは自分で書かなくても NotFoundException みたいなよくある Exception の場合は nestjs 側でつけてくれるらしい。覚えておこう。

@Injectable()
export class CoffeesService {
  findOne(id: string) {
    const coffee = this.coffees.find((item) => item.id === +id);
    if (!coffee) {
      throw new NotFoundException(`Coffee #${id} not found`);
    }
    return coffee;
  }
}

Module を作成する

Controller, Service などを XxxModule にまとめる。

AppModule は各ドメインの Controller, Service を直接見に行かず、Module を介して操作する。

@Module({
  imports: [CoffeesModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

iOS の開発でよく見る Viper 感ある

https://qiita.com/hicka04/items/09534b5daffec33b2bec

Dto

処理に必要なデータを記述した Dto クラスを定義する。

微妙に理解浅い。フワッとしてる。

export class CreateCoffeeDto {
  readonly name: string;
  readonly brand: string;
  readonly flavors: string[];
}

Validation

ValidationPipe を足すと class-validator とか諸々効くようになるみたい。詳しくはわからん。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Dto でバリデーション

Dto の記述なんでクラスで書くんやろ、と思ったけど、バリデーションかける機構が class を前提としているから、とかありそう。

以下を書くだけで 400 系の bad request が飛ぶ

import { IsString } from 'class-validator';

export class CreateCoffeeDto {
  @IsString()
  readonly name: string;
  @IsString()
  readonly brand: string;
  @IsString({ each: true })
  readonly flavors: string[];
}

Dto を継承すれば少し性質を変えられる

PartialType とかいうのを使って、フィールドが optional のバージョンの Dto を作る。

import { PartialType } from '@nestjs/mapped-types';
import { CreateCoffeeDto } from './create-coffee.dto';

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {}

ValidationPipewhitelist を指定すると、validation されていない値を捨ててくれる。
見た感じ Dto を通った時に捨てられる。保存もされない。

さらに forbidNonWhitelisted: true を指定すると Forbidden エラーにできる。

 new ValidationPipe({
     whitelist: true,
     forbidNonWhitelisted: true,
   }),

transform: true を設定すると、型変換を試みてくれる。例えば id を number で受けるとインスタンスレベルで number にしてくれる、らしい。(JS のオブジェクトよく分かってない)

これデフォルトで有効にしといてくれても良くない? とは思った。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
      forbidNonWhitelisted: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

勘所的には、リクエスト来る時は全部文字列だから注意。

TypeORM の導入

パッケージを install して、AppModule に追加する。

@Module({
  imports: [
    CoffeesModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      // ....
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

synchronize: true を指定すると、@Entity をつけたクラスが同期される。

@Module({
  imports: [
    CoffeesModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'pass123',
      database: 'postgres',
      autoLoadEntities: true,
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Repository

自分で Repository 実装するのかなと思いきや、Entity 用のクラスを実装しておけば、Repository<SomeEntity> で repository になるみたい。宣言的に書いとくだけでいいのは結構いい。

これで依存が Controller=> Service => Repository って感じになった。

Relation

NoSQL ばっかりでアプリ作ってきたので一番詰まりそうな箇所。JoinTable とかアノテーション使って書く。

@Entity('coffees') // sql table === 'coffees'
export class Coffee {
  @JoinTable()
  @ManyToMany((type) => Flavor, (flavor) => flavor.coffees)
  flavors: Flavor[];
}

保存時は関連のある Relation を事前に取得して Dto と合わせて保存する。

  async create(createCoffeeDto: CreateCoffeeDto) {
    const flavors = await Promise.all(
      createCoffeeDto.flavors.map((name) => this.preloadFlavorByName(name)),
    );
    const coffee = this.coffeeRepository.create({
      ...createCoffeeDto,
      flavors: flavors,
    });
    return this.coffeeRepository.save(coffee);
  }

以下のことを思った。(主に RDB の感想)

  • 上記のような関連を持たせることで変更が難しくなるのではないか
  • 仮にサーバーサイドを置き換えるプロジェクトがあるとすると、あらゆるデータが関連していると、部分的移行が不可能になるのではないか
  • リレーションを使う場面はそんなに沢山あるのか?

一旦ドップリ RDB の世界に浸かる。

ログインするとコメントできます