NestJS触ってみる(基礎)
環境
$ sw_vers
ProductName: macOS
ProductVersion: 12.2
BuildVersion: 21D49
$ uname -m
arm64
$ node -v
v16.14.0
$ yarn -v
1.22.17
プロジェクトを作ってみる
$ mkdir project
$ cd project
$ yarn init -y
$ yarn add @nestjs/cli
$ yarn nest new backend
? Which package manager would you ❤️ to use?
npm
❯ yarn
pnpm
$ cd backend
文末にセミコロンを付けたくないので.prettierrc
を編集
{
"singleQuote": true,
"trailingComma": "all",
+ "semi": false
}
ただgenerate
コマンドでファイルを作った場合に自動的にセミコロンが付いてくる…
yarn format
で都度フォーマットすればいいけど、他に方法ないんやろか…
下記のファイルは一旦不要なので削除
src/app.controller.spec.ts
src/app.controller.ts
src/app.service.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 {}
Modules
モジュールを作成
(下記ではitems
モジュールを作成している)
$ yarn nest g module items
-
src/items/items.module.ts
が作成される -
app.module.ts
のimports
にItemsModule
が追加される
Controllers
コントローラーを作成
(下記ではitems
コントローラーを作成している)
$ yarn nest g controller items --no-spec
-
src/items/items.controller.ts
が作成される -
items.module.ts
のcontrollers
にItemsController
が追加される
適当に特定のパスに対応するハンドラーを作ってみる
(下記ではitemsパス
でGET
の場合にfindAllメソッド
が反応するように作成している)
import { Controller, Get } from '@nestjs/common'
@Controller('items')
export class ItemsController {
@Get()
findAll() {
return 'This is findAll'
}
}
サーバー起動して確認
$ yarn start:dev
レスポンス確認
curl http://localhost:3000/items
This is findAll
Providers
サービスを作成
(下記ではitemsサービスを作成している)
$ yarn nest g service items --no-spec
-
src/items/items.service.ts
が作成される -
items.module.ts
のproviders
にItemsService
が追加される
サービスに試しにメソッドを定義
import { Injectable } from '@nestjs/common'
@Injectable()
export class ItemsService {
findAll() {
return 'This is ItemsService'
}
}
サービスに定義したメソッドをコントローラーで使用
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()
}
}
サーバー起動して確認
$ yarn start:dev
レスポンス確認
$ curl http://localhost:3000/items
This is ItemsService
モデル(型)を作成
(下記ではitemモデル[型]を作成している)
export type Item = {
id: string
name: string
price: number
description: string
status: 'ON_SALE' | 'SOLD_OUT'
}
CRUD機能
Create
サービスにcreateメソッド
を定義
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メソッド
を定義
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)
}
}
サーバー起動して確認
$ yarn start:dev
POST成功するか確認
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
全件取得
// --------- 略 ---------
findAll(): Item[] {
return this.items
}
// --------- 略 ---------
// --------- 略 ---------
@Get()
findAll(): Item[] {
return this.itemsService.findAll()
}
// --------- 略 ---------
GETできるか確認
$ curl http://localhost:3000/items | jq
[
{
"id": "test",
"name": "PC",
"price": "50000",
"description": "綺麗です",
"status": "ON_SALE"
}
]
個別取得
// --------- 略 ---------
findById(id: string): Item {
return this.items.find((item) => item.id === id)
}
// --------- 略 ---------
// --------- 略 ---------
@Get(':id')
findById(@Param('id') id: string): Item {
return this.itemsService.findById(id)
}
// --------- 略 ---------
GETできるか確認
$ curl http://localhost:3000/items/test | jq
[
{
"id": "test",
"name": "PC",
"price": "50000",
"description": "綺麗です",
"status": "ON_SALE"
}
]
Update
(下記ではstatus
をSOLD_OUT
にするメソッドを定義している)
// --------- 略 ---------
updateStatus(id: string): Item {
const item = this.findById(id)
item.status = 'SOLD_OUT'
return item
}
// --------- 略 ---------
// --------- 略 ---------
@Patch(':id')
updateStatus(@Param('id') id: string): Item {
return this.itemsService.updateStatus(id)
}
// --------- 略 ---------
PATCH確認
$ curl -X PATCH http://localhost:3000/items/test | jq
{
"id": "test",
"name": "PC",
"price": "50000",
"description": "綺麗です",
"status": "SOLD_OUT"
}
Delete
// --------- 略 ---------
delete(id: string): void {
this.items = this.items.filter((item) => item.id !== id)
}
// --------- 略 ---------
// --------- 略 ---------
@Delete(':id')
delete(@Param('id') id: string): void {
this.itemsService.delete(id)
}
// --------- 略 ---------
DELETE確認
$ curl -X DELETE http://localhost:3000/items/test
$ curl http://localhost:3000/items
[]
DTOを使用したリファクタリング
下記作成
src
└── items
└── dto
└── create-item.dto.ts ←新規作成
export class CreateItemDto {
id: string
name: string
price: number
description: string
}
コントローラーのcreate
メソッドをリファクタリング
// --------- 略 ---------
@Post()
create(@Body() createItemDto: CreateItemDto): Item {
return this.itemsService.create(createItemDto)
}
// --------- 略 ---------
サービスのcreate
メソッドをリファクタリング
// --------- 略 ---------
create(createItemDto: CreateItemDto): Item {
const item: Item = {
...createItemDto,
status: 'ON_SALE',
}
this.items.push(item)
return item
}
// --------- 略 ---------
id
にuuidを使用して自動採番されるようにする
$ yarn add uuid
下記変更
export class CreateItemDto {
- id: string
name: string
price: number
description: string
}
// --------- 略 ---------
+ 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
}
// --------- 略 ---------
バリデーション
NestJS
でバリデーションを行うにはPipeを使う
リクエストで来るid
がuuid
の形式かをバリデーション
Built-in pipesのParseUUIDPipe
を使用
// --------- 略 ---------
@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を使用
$ yarn add class-validator class-transformer
class-transformerも必要なので入れておく
DTO
クラスのプロパティにclass-validator
とclass-transformer
のデコレーターを付けることでバリデーションルールを定義
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
}
定義したバリデーションルールをグローバルで適用
+ 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()
バリデーションされるか確認
$ 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"
}
例外処理
存在しないid
でリクエストが来たときの例外処理の例
Built-in HTTP exceptionsのNotFoundException
を使用
// --------- 略 ---------
findById(id: string): Item {
const found = this.items.find((item) => item.id === id)
if (!found) {
throw new NotFoundException()
}
return found
}
// --------- 略 ---------
DB使用して色々やってみる
続きは上記で書いていきます。