NestJS DTO / ValidationPipe 入門
DTOとValidationPipeの役割を理解し、NestJSで入力検証付きのPOST APIを最小構成で実装する
前回の記事ではNestJSの基礎を学んだ。
なぜ最初にDTO / ValidationPipeか
APIで受け取るデータがDTOに定義された形になっているかValidationPipeを用いて検証を行うことで、不正な挙動を回避しつつ、Controller以降では入力値が最低限まともである前提で書くことができるようにする。 実際、NestJS実務ではDTO / ValidationPipeはかなり高頻度で登場する。
- CRUD/API/認証/Swaggerの土台になる
- APIの入力境界を最初に固められる
- 後からバグ修正するより、入口で弾く方が安い
ここを飛ばすと、次のようなコードを見た時に詰まりやすい。
create(@Body() dto: CreateUserDto)- なぜDTOを挟むのか
- なぜinterfaceではなくclassなのか
- どこでバリデーションされているのか
そもそもDTOとValidationPipeとは
DTO(Data Transfer Object)
NestJSでのDTOは、APIで受け取るデータの形を定義するためのクラス。
POST /usersなら「idとnameを受け取る」のように入力項目を明確にするclass-validatorのDecoratorを使って、入力ルール(必須/文字数/メール形式)をDTOに書ける- EntityやDBモデルとは役割が違い、DTOはあくまで「API入口の契約」を表す
要するに、
DTO = API入口で受け取るデータ仕様ValidationPipe
ValidationPipeは、@Body() で受け取る値をDTOルールで検証する仕組み。
- DTOに書いた
@IsEmail()や@Length()を実行する - 不正な入力ならController処理に入る前に400エラーで止める
whitelistなどのオプションで、余計な値を落とす/弾くを制御できる
要するに、
ValidationPipe = DTOルールを実際に適用して入力検証する実行装置今回やること
nestjs-basics で作成した api-sample に対して、DTO / ValidationPipeを追加する。(既存プロジェクトに機能追加)
サンプル実装手順
- 前回記事で作成した既存プロジェクトへ移動
cd nestjs-practice/api-sample- 必要ライブラリ追加(DTO/Validation用)
npm install class-validator class-transformerclass-validator:@Length()などの検証ルール本体class-transformer: 受け取ったJSONをDTOクラスへ変換するために利用
- DTO/Validationを差し込む先(controller/service/module)を用意:
users機能があるか確認
前回用意した users 機能に「DTO + Validation」を追加する。
users がまだ無い場合は次を実行する。
nest g module users
nest g controller users
nest g service usersmodule: users機能の配線controller: HTTP入口(ルーティング)service: 処理の中身(ロジック)
- DTO作成
受け入れる入力の契約を明文化する。ここで id と name のルールを固定することで、以降の処理が前提を持って書ける。
src/users/dto/create-user.dto.ts を作成する。
import { IsString, Length } from "class-validator";
export class CreateUserDto {
@IsString()
@Length(1, 30)
id: string;
@IsString()
@Length(1, 50)
name: string;
}ここで重要なのは次の理解。
DTO = API入力値の型 + バリデーション定義- ValidationPipe設定
src/main.ts に ValidationPipe を追加する。
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({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// ここまで追加
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();この手順の意図は「DTOに書いたルールを全APIで実際に有効化する」こと。
DTOだけ作っても、Pipeを設定しない限り自動検証は走らない。
- Controller修正
src/users/users.controller.ts を次のようにする。
import { Body, Controller, Post } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
@Controller("users")
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
return {
message: "user created",
data: dto,
};
}
}この手順の意図は「@Body() 入力をDTO経由で受ける」こと。
create(@Body() dto: CreateUserDto) にすることで、ValidationPipeがDTOルールを適用できる。
- 動作確認
起動:
npm run start:devこの手順の意図は「正常系と異常系の両方で、検証が効いていることを確認する」こと。
200だけでなく400を確認して初めて、設定が正しく効いていると言える。
正常系:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"id":"u_3","name":"Taro"}'バリデーションエラー確認:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"id":"","name":""}'2つ目は 400 エラーになるはず。
ここで理解すべきこと(超重要)
DTOの役割
DTOはAPIで受け取るデータ定義であり、次の意味を持つ。
API入口専用の型EntityやDBモデルと同一にしないのが基本。
ValidationPipeの役割
ValidationPipeは、DTOに書いたルールで入力値を検証する仕組み。
Controllerに入る前に、入力をチェックするwhitelist の意味
whitelist: trueDTOに存在しない値を削除する。
forbidNonWhitelisted の意味
forbidNonWhitelisted: trueDTOに存在しない値が来た時点で400エラーにする。
実務では「想定外入力を静かに通さない」ために重要。
transform の意味
transform: true@Body() の値をDTOクラスへ変換する。
数値変換やPipe連携(ParseIntPipe など)と組み合わせる時に有効。
よくある落とし穴
- DTOを
interfaceで定義してしまう(Decoratorが効かないのでclassを使う) ValidationPipeを設定しておらず、Decoratorを書いても検証されない- DTOを作っただけでController側の
@Body()型をDTOにしていない whitelistだけ有効でforbidNonWhitelistedが無効だと、余計な値が黙って落ちる