[Scrap] NestJS 勉強メモ
連休で時間取れるので、オンラインコースをやってみることにする。$85 高いかもと思ったが、公式が出してるやつだし、英語分かりやすいしポチった。
- 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 感ある
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) {}
ValidationPipe
で whitelist
を指定すると、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 の世界に浸かる。