Closed14

NestJS触ってみる(基礎)

ピン留めされたアイテム
keisuke-333keisuke-333

環境

terminal
$ sw_vers
ProductName:	macOS
ProductVersion:	12.2
BuildVersion:	21D49

$ uname -m
arm64

$ node -v
v16.14.0

$ yarn -v
1.22.17
keisuke-333keisuke-333

プロジェクトを作ってみる

terminal
$ mkdir project
$ cd project

$ yarn init -y

$ yarn add @nestjs/cli
$ yarn nest new backend
? Which package manager would you ❤️  to use?
  npmyarn
  pnpm
terminal
$ cd backend
keisuke-333keisuke-333

文末にセミコロンを付けたくないので.prettierrcを編集

.prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
+ "semi": false
}

ただgenerateコマンドでファイルを作った場合に自動的にセミコロンが付いてくる…
yarn formatで都度フォーマットすればいいけど、他に方法ないんやろか…

keisuke-333keisuke-333

下記のファイルは一旦不要なので削除

  • src/app.controller.spec.ts
  • src/app.controller.ts
  • src/app.service.ts

app.module.tsから上記のファイルの読み込みを削除

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 {}

keisuke-333keisuke-333

Modules

モジュールを作成
(下記ではitemsモジュールを作成している)

terminal
$ yarn nest g module items
  • src/items/items.module.tsが作成される
  • app.module.tsimportsItemsModuleが追加される
keisuke-333keisuke-333

Controllers

コントローラーを作成
(下記ではitemsコントローラーを作成している)

terminal
$ yarn nest g controller items --no-spec
  • src/items/items.controller.tsが作成される
  • items.module.tscontrollersItemsControllerが追加される

適当に特定のパスに対応するハンドラーを作ってみる
(下記ではitemsパスGETの場合にfindAllメソッドが反応するように作成している)

items.controller.ts
import { Controller, Get } from '@nestjs/common'

@Controller('items')
export class ItemsController {
  @Get()
  findAll() {
    return 'This is findAll'
  }
}

サーバー起動して確認

terminal
$ yarn start:dev

レスポンス確認

terminal(別画面とかで)
curl http://localhost:3000/items
This is findAll
keisuke-333keisuke-333

Providers

サービスを作成
(下記ではitemsサービスを作成している)

terminal
$ yarn nest g service items --no-spec
  • src/items/items.service.tsが作成される
  • items.module.tsprovidersItemsServiceが追加される

サービスに試しにメソッドを定義

items.service.ts
import { Injectable } from '@nestjs/common'

@Injectable()
export class ItemsService {
  findAll() {
    return 'This is ItemsService'
  }
}


サービスに定義したメソッドをコントローラーで使用

items.controller.ts
import { Controller, Get } from '@nestjs/common'
import { ItemsService } from './items.service'

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}
  @Get()
  findAll() {
    return this.itemsService.findAll()
  }
}

サーバー起動して確認

terminal
$ yarn start:dev

レスポンス確認

terminal(別画面とかで)
$ curl http://localhost:3000/items
This is ItemsService
keisuke-333keisuke-333

モデル(型)を作成
(下記ではitemモデル[型]を作成している)

item.model.ts
export type Item = {
  id: string
  name: string
  price: number
  description: string
  status: 'ON_SALE' | 'SOLD_OUT'
}
keisuke-333keisuke-333

CRUD機能

Create

サービスにcreateメソッドを定義

items.service.ts
import { Injectable } from '@nestjs/common'
import { Item } from './item.model'

@Injectable()
export class ItemsService {
  private items: Item[] = []

  findAll() {
    return 'This is ItemsService'
  }

  create(item: Item): Item {
    this.items.push(item)
    return item
  }
}

コントローラーにcreateメソッドを定義

items.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common'
import { Item } from './item.model'
import { ItemsService } from './items.service'

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get()
  findAll() {
    return this.itemsService.findAll()
  }

  @Post()
  create(
    @Body('id') id: string,
    @Body('name') name: string,
    @Body('price') price: number,
    @Body('description') description: string,
  ): Item {
    const item: Item = {
      id,
      name,
      price,
      description,
      status: 'ON_SALE',
    }
    return this.itemsService.create(item)
  }
}

サーバー起動して確認

terminal
$ yarn start:dev

POST成功するか確認

terminal(別画面とかで)
curl -X POST -d "id=test&name=PC&price=50000&description=綺麗です" http://localhost:3000/items | jq

{
  "id": "test",
  "name": "PC",
  "price": "50000",
  "description": "綺麗です",
  "status": "ON_SALE"
}

Read

全件取得

items.service.ts
// --------- 略 ---------
findAll(): Item[] {
  return this.items
}
// --------- 略 ---------
items.controller.ts
// --------- 略 ---------
@Get()
findAll(): Item[] {
  return this.itemsService.findAll()
}
// --------- 略 ---------

GETできるか確認

terminal
$ curl http://localhost:3000/items | jq

[
  {
    "id": "test",
    "name": "PC",
    "price": "50000",
    "description": "綺麗です",
    "status": "ON_SALE"
  }
]

個別取得

items.service.ts
// --------- 略 ---------
findById(id: string): Item {
  return this.items.find((item) => item.id === id)
}
// --------- 略 ---------
items.controller.ts
// --------- 略 ---------
@Get(':id')
findById(@Param('id') id: string): Item {
  return this.itemsService.findById(id)
}
// --------- 略 ---------

GETできるか確認

terminal
$ curl http://localhost:3000/items/test | jq

[
  {
    "id": "test",
    "name": "PC",
    "price": "50000",
    "description": "綺麗です",
    "status": "ON_SALE"
  }
]

Update

(下記ではstatusSOLD_OUTにするメソッドを定義している)

items.service.ts
// --------- 略 ---------
updateStatus(id: string): Item {
  const item = this.findById(id)
  item.status = 'SOLD_OUT'
  return item
}
// --------- 略 ---------
items.controller.ts
// --------- 略 ---------
@Patch(':id')
updateStatus(@Param('id') id: string): Item {
  return this.itemsService.updateStatus(id)
}
// --------- 略 ---------

PATCH確認

terminal
$ curl -X PATCH http://localhost:3000/items/test | jq

{
  "id": "test",
  "name": "PC",
  "price": "50000",
  "description": "綺麗です",
  "status": "SOLD_OUT"
}

Delete

items.service.ts
// --------- 略 ---------
delete(id: string): void {
  this.items = this.items.filter((item) => item.id !== id)
}
// --------- 略 ---------
items.controller.ts
// --------- 略 ---------
@Delete(':id')
delete(@Param('id') id: string): void {
  this.itemsService.delete(id)
}
// --------- 略 ---------

DELETE確認

terminal
$ curl -X DELETE http://localhost:3000/items/test

$ curl http://localhost:3000/items
[]
keisuke-333keisuke-333

DTOを使用したリファクタリング

下記作成

ディレクトリ図
src
└── items
    └── dto
        └── create-item.dto.ts ←新規作成
create-item.dto.ts
export class CreateItemDto {
  id: string
  name: string
  price: number
  description: string
}

コントローラーのcreateメソッドをリファクタリング

items.controller.ts
// --------- 略 ---------
@Post()
create(@Body() createItemDto: CreateItemDto): Item {
  return this.itemsService.create(createItemDto)
}
// --------- 略 ---------

サービスのcreateメソッドをリファクタリング

items.service.ts
// --------- 略 ---------
create(createItemDto: CreateItemDto): Item {
  const item: Item = {
    ...createItemDto,
    status: 'ON_SALE',
  }
  this.items.push(item)
  return item
}
// --------- 略 ---------
keisuke-333keisuke-333

iduuidを使用して自動採番されるようにする

terminal
$ yarn add uuid

下記変更

create-item.dto.ts
export class CreateItemDto {
- id: string
  name: string
  price: number
  description: string
}
items.service.ts
// --------- 略 ---------

+ import { v4 as uuid } from 'uuid'

// --------- 略 ---------

create(createItemDto: CreateItemDto): Item {
  const item: Item = {
+   id: uuid(),
    ...createItemDto,
    status: 'ON_SALE',
  }
  this.items.push(item)
  return item
}

// --------- 略 ---------
keisuke-333keisuke-333

バリデーション

NestJSでバリデーションを行うにはPipeを使う

リクエストで来るiduuidの形式かをバリデーション
Built-in pipesParseUUIDPipeを使用

items.controller.ts
// --------- 略 ---------

@Get(':id')
findById(@Param('id', ParseUUIDPipe) id: string): Item {
  return this.itemsService.findById(id)
}

// --------- 略 ---------

@Patch(':id')
updateStatus(@Param('id', ParseUUIDPipe) id: string): Item {
  return this.itemsService.updateStatus(id)
}

// --------- 略 ---------

@Delete(':id')
delete(@Param('id', ParseUUIDPipe) id: string): void {
  this.itemsService.delete(id)
}

// --------- 略 ---------

@Paramの第二引数にPipeを指定


Class validator

入力値のバリデーションにclass-validatorを使用

terminal
$ yarn add class-validator class-transformer

class-transformerも必要なので入れておく

DTOクラスのプロパティにclass-validatorclass-transformerのデコレーターを付けることでバリデーションルールを定義

create-item.dto.ts
import { Type } from 'class-transformer'
import { IsInt, IsNotEmpty, IsString, MaxLength, Min } from 'class-validator'

export class CreateItemDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(40)
  name: string

  @IsInt()
  @Min(1)
  @Type(() => Number)
  price: number

  @IsString()
  @IsNotEmpty()
  description: string
}

定義したバリデーションルールをグローバルで適用

main.ts
+ import { ValidationPipe } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

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

バリデーションされるか確認

terminal
$ curl -X POST -d "name=&price=0&description=" http://localhost:3000/items | jq

{
  "statusCode": 400,
  "message": [
    "name should not be empty",
    "price must not be less than 1",
    "description should not be empty"
  ],
  "error": "Bad Request"
}
keisuke-333keisuke-333

例外処理

存在しないidでリクエストが来たときの例外処理の例
Built-in HTTP exceptionsNotFoundExceptionを使用

items.service.ts
// --------- 略 ---------
findById(id: string): Item {
  const found = this.items.find((item) => item.id === id)
  if (!found) {
    throw new NotFoundException()
  }
  return found
}
// --------- 略 ---------
このスクラップは2022/04/18にクローズされました