0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【NestJS】バリデーションが全く効かなくて2時間ハマった

Posted at

事の発端

個人でタスク管理アプリを作ろうと思い、NestJS + GraphQLに挑戦することに。「まずはタスク作成のAPIから作るか」と意気込んでDTOを書きました。

import { IsNotEmpty, IsString } from 'class-validator';
import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class CreateTaskInput {
  @Field()
  @IsNotEmpty()
  @IsString()
  name: string;

  @Field()
  @IsNotEmpty()
  dueDate: string;

  @Field({ nullable: true })
  @IsOptional()
  description?: string;
}

よし、これで空文字は弾かれるはず!と思ってPostmanでテスト...

絶望の瞬間

POST /graphql
{
  "query": "mutation { createTask(input: { name: \"\", dueDate: \"\" }) { id name } }"
}

結果...

{
  "data": {
    "createTask": {
      "id": "1",
      "name": ""  // 😱 空文字が通ってる...
    }
  }
}

は?なんで???

ググりまくりの始まり

疑ったポイント1: デコレーターの書き方

「NestJS バリデーション 効かない」でGoogle検索しまくり...

// もしかしてこう?
@IsNotEmpty()
@Field()
name: string;

// それともこう?
@Field()
@IsNotEmpty()
name: string;

結果: どっちでも変わらず

疑ったポイント2: パッケージが足りない?

Stack Overflowで「class-validatorが必要」という記事を発見。

npm install class-validator class-transformer

結果: すでに入ってた

疑ったポイント3: GraphQLだから効かない?

「もしかしてGraphQLでは別の書き方?」と思ってREST APIでも試してみる。

// REST APIでも試してみる
@Post()
createTask(@Body() input: CreateTaskInput) {
  console.log('input:', input); // { name: "", dueDate: "" }
}

結果: RESTでも効かない!

ついに答えを発見

もう諦めかけた時、Qiitaの記事で救いの一文を発見...

「NestJSでバリデーションを使うにはValidationPipeの設定が必要です」

え、そんなの必要だったの!?

真犯人発見

// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; // ← これを追加
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // ← この1行が抜けてた!!!
  app.useGlobalPipes(new ValidationPipe());
  
  await app.listen(3000);
}
bootstrap();

設定後のテスト

再び同じリクエストを送信...

{
  "errors": [
    {
      "message": "Bad Request Exception",
      "extensions": {
        "code": "BAD_USER_INPUT",
        "response": {
          "statusCode": 400,
          "message": [
            "name should not be empty",
            "dueDate should not be empty"
          ],
          "error": "Bad Request"
        }
      }
    }
  ]
}

キターーーーー!!! 🎉

なぜハマったのか

実はNestJSでは:

リクエスト → Pipe → Controller → Service

この流れで処理されるんですが、Pipeが設定されてないと検証が実行されないんです。

// DTOのデコレーターは「ルールの定義」
@IsNotEmpty() // ← 「空文字はダメ」というルール

// ValidationPipeは「ルールの実行者」
app.useGlobalPipes(new ValidationPipe()); // ← 「ルールをチェックして」

つまり、ルールブックを作っても審判(ValidationPipe)がいないと試合にならない状態だったわけです。

さらにハマったポイント

設定したは良いものの、今度はこんなエラーが...

Error: Cannot determine GraphQL input type for CreateTaskInput

なぜかGraphQLのスキーマ生成でコケる。調べてみると...

// ダメな例
app.useGlobalPipes(new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
}));

GraphQLではtransform: trueを設定するとスキーマ生成でバグることがあるらしい。結局シンプルに:

app.useGlobalPipes(new ValidationPipe());

これで落ち着きました。

他の適用方法も試してみた

コントローラー単位

@Controller('tasks')
@UsePipes(ValidationPipe)
export class TaskController {
  // このコントローラーだけバリデーション
}

メソッド単位

@Post()
createTask(@Body(ValidationPipe) input: CreateTaskInput) {
  // このメソッドだけバリデーション
}

でも結局、グローバル設定が一番楽でした。

実際にやらかした時のバグ

ValidationPipeを設定する前、こんなコードを書いてました:

@Mutation(() => Task)
async createTask(@Args('input') input: CreateTaskInput) {
  // input.name が空文字でもここに到達
  const slug = input.name.toLowerCase().replace(/\s+/g, '-');
  //                    ↑
  //           Cannot read property 'toLowerCase' of null
  //           → ローカルで500エラー!
}

開発中にエラーが出まくって「なんで???」状態でした...😭

学んだこと

  1. DTOだけ書いてもバリデーションは効かない
  2. ValidationPipeの設定は必須
  3. main.tsでグローバル設定が基本
  4. GraphQLでは設定オプションに注意

今では当たり前に設定してること

// main.ts(テンプレート化した)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // バリデーション(必須)
  app.useGlobalPipes(new ValidationPipe());
  
  // CORS(開発時)
  app.enableCors();
  
  await app.listen(3000);
}

最後に

ValidationPipeの設定だけは絶対に忘れずにと肝に銘じておきます

「なんでバリデーション効いてないの?」って2時間悩むのは結構しんどいです...😅


追記:よく使うバリデーションルール

import { 
  IsNotEmpty, 
  IsString, 
  IsEmail, 
  IsOptional,
  MinLength,
  MaxLength,
  IsDateString 
} from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @IsNotEmpty({ message: '名前は必須です' })
  @IsString()
  @MinLength(2, { message: '名前は2文字以上で入力してください' })
  @MaxLength(50)
  name: string;

  @Field()
  @IsEmail({}, { message: '正しいメールアドレスを入力してください' })
  email: string;

  @Field({ nullable: true })
  @IsOptional()
  @IsDateString()
  birthDate?: string;
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?