この記事は 株式会社サイバー・バズ Advent calendar 2021 8日目の記事です。
概要
Nest.jsを使っている時に、frontから受け取ったRequestを使ってゴネゴネしている時です。
すると、、
「あれ、、、落ちる。。」
「想定した型と合っていない。。。」
ということがありました。
その時の話です。
例
検索処理を例にあげます。
class SearchItemDTO {
minPrice: number
maxPrice: number
page: number
limit: number
}
@Get()
async getItems(@Query() searchItemDTO: SearchItemDTO){
// 検索処理Service
}
以下のRequestをgetItemsに投げました。
curl 'http://localhost:3001/items?minPrice=3000&maxPrice=10000&page=1&limit=10'
ここでgetItemsに実際に渡ってきたsearchItemDTOを確認してみると、
全てstringになっており、SearchItemDTOの型と乖離していることが分かります。
{ minPrice: '3000', maxPrice: '10000', page: '1', limit: '10' }
minPriceやmaxPriceを使ってnumber前提で計算処理などを書いてしまうと、落ちる危険があります。
Pipes
Nestには、Pipesというものが存在します。
PipesはクライアントからのRequestをControllerに渡す前に処理を挟むことができます。
Documentでも取り上げられているように、変換とバリデーションの二つが主なユースケースのようです。
Built-in pipes
汎用的なPipesについては、あらかじめBuilt-inで@nestjs/commonからimportして使うことができます。
今回はParseIntPipeが使えそうです。
ParseIntPipeを使ってgetItemsを書き換えてみます。
@Get()
async getItems(
@Query('minPrice', ParseIntPipe) minPrice: number,
@Query('maxPrice', ParseIntPipe) maxPrice: number,
@Query('page', ParseIntPipe) page: number,
@Query('limit', ParseIntPipe) limit: number
){
// 検索処理Service
}
再度getItemsに渡ってきたプロパティを確認してみると、
{ minPrice: 3000, maxPrice: 10000, page: 1, limit: 10 }
想定通りnumberに変換されています。
これで目的は果たせましたが、パラメータが増えるたびにParseIntPipeを増やす必要があり、
item.controller.tsファイルが肥大化してしまうおそれがあります。
ユーザ定義pipe
pipeは自作することもできます。
まずDTOのうち
変換前をSearchItemDTO
変換後をSearchItemTransformDTO
として実際の型と乖離がないように再定義してみます。
export class SearchItemDTO {
minPrice: string
maxPrice: string
page: string
limit: string
}
export interface SearchItemTransformDTO {
minPrice: number
maxPrice: number
page: number
limit: number
}
次にpipe処理です。
import { PipeTransform, Injectable } from '@nestjs/common'
@Injectable()
export class SearchItemPipe implements PipeTransform<SearchItemDTO, SearchItemTransformDTO> {
transform(searchItemDTO: SearchItemDTO): SearchItemTransformDTO {
return Object.entries(searchItemDTO).reduce((acc, cur) => {
const key = cur[0]
const value = ['minPrice', 'maxPrice', 'page', 'limit'].includes(key) ? parseInt(cur[1]) : cur[1]
return { ...acc, [key]: value }
}, {})
}
}
SearchItemPipeをitem.controller.tsに反映させます。
@Get()
async getItems(@Query(SearchItemPipe) searchItemTransformDTO: SearchItemTransformDTO){
// 検索処理Service
}
すっきり書けました。
パラメータが増えた場合は、SearchItemPipeを編集すれば対応できます。
これで定義したDTOと実際の値との間で乖離が無くなりました。
他にも方法はあるかと思いますが、一例として参考にしていただければと思います。