NestJS DTO / ValidationPipe 入門
DTOとValidationPipeの役割を理解し、NestJSで入力検証付きのPOST APIを最小構成で実装する
NestJSの基礎では、Controller / Service / Module の最小構成を学んだ。
なぜ最初に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の基礎で作成した api-sample に対して、DTO / ValidationPipeを追加する。(既存プロジェクトに機能追加)
ファイル構成(今回触る場所)
NestJSの基礎で作ったプロジェクトへの追加なので、触るファイルは少ない。
api-sample/
└─ src/
├─ main.ts ← ValidationPipe設定を追加(編集)
└─ users/
├─ dto/
│ └─ create-user.dto.ts ← DTO新規作成
└─ users.controller.ts ← @Body()をDTO経由に変更(編集)users.service.ts / users.module.ts / app.module.ts は前回のまま触らない。
サンプル実装手順
- NestJSの基礎で作成した既存プロジェクトへ移動
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) // 長さチェック:1文字以上30文字以内
id!: string;
@IsString() // 型チェック:文字列であること(実行時)
@Length(1, 50) // 長さチェック:1文字以上50文字以内
name!: string;
}! は「このプロパティはコンストラクタでは代入しないが、実行時には値が入る」ことをTypeScriptに伝える記法。DTOではNestJSがリクエストBodyから値を入れるため、strictPropertyInitialization が有効な環境では付けておくとエラーを避けられる。
: string もTypeScriptによる型チェックだが、これはコンパイル時のチェック。HTTPリクエストのBodyはTypeScriptから見れば unknown であり、実際に受け取った値が文字列かどうかは実行時にならないとわからない。@IsString() がその実行時の型チェックを担う。
| タイミング | 役割 | |
|---|---|---|
: string | コンパイル時 | コード内での型の整合性チェック |
@IsString() | 実行時 | リクエストBodyの実際の値チェック |
ここで重要なのは次の理解。
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, // DTOに定義していないプロパティを自動で除去
transform: true, // リクエストBodyをDTOクラスのインスタンスに変換
forbidNonWhitelisted: true, // DTOにないプロパティが来たらエラー(除去でなく拒否)
}),
);
// ここまで追加
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();この手順の意図は「DTOに書いたルールを全APIで実際に有効化する」こと。
DTOだけ作っても、Pipeを設定しない限り自動検証は走らない。
src/main.ts に書く理由は、アプリ全体に一度だけ適用するため。app.useGlobalPipes(...) はアプリケーションインスタンスに対してグローバルな設定を行うAPIで、これを呼べるのはアプリを生成した main.ts(起動ファイル)だけ。AppModule や UsersModule はDIコンテナの世界(NestJSの管理下)にあるが、useGlobalPipes はその外側——アプリ起動時の初期化処理——に属する。すべてのルートで検証を効かせたいなら、個々のModuleやControllerに書くより、起動時に一箇所で設定するのが最も確実で重複がない。
なお、Controllerやルート単位で個別に適用することもできる。その場合は @UsePipes(new ValidationPipe(...)) デコレータをControllerクラスやメソッドに付ける。特定のエンドポイントだけ異なる検証ルールを使いたいときに有効だが、設定が分散するため「全APIで同じルールを使う」場面では main.ts に書くグローバル設定の方がシンプル。
「ユーザー追加」と「商品追加」のように入力項目が異なるエンドポイントが複数あっても、ValidationPipeを追加する必要はなく、DTOを増やすだけでよい。ValidationPipeは @Body() の型注釈を元に「どのDTOのルールで検証するか」を自動で判断する。
// UsersController → CreateUserDto のルールで検証
@Post()
create(@Body() dto: CreateUserDto) { ... }
// ProductsController → CreateProductDto のルールで検証
@Post()
create(@Body() dto: CreateProductDto) { ... }ValidationPipeは1つのままで、検証の定義はDTO側に持たせるのがNestJSの設計。機能を追加するたびにDTOを用意するだけで、main.ts の設定には触れなくてよい。
- 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 エラーになるはず。
参考
シリーズの次へ
DBにユーザーを保存するCRUDへ進む場合は NestJSでCRUD APIを作る(Prisma + SQLite)。