class‑validatorの IsMongoId : MongoDB ObjectId検証

class‑validatorの IsMongoId : MongoDB ObjectId検証

D
dongAuthor
5 min read

NestJSでAPIを開発していると、MongoDBのObjectId形式のパラメーターを検証する必要がしばしばあります。誤った形式のIDが来るとデータベースのクエリエラーが発生し、ユーザーには不親切なエラーメッセージが伝わります。

class‑validator@IsMongoId() デコレーターを使えば、この問題を簡単に解決できます。DTOにデコレーターを1つだけ追加すれば、MongoDB ObjectIdの検証が自動的に行われ、誤ったリクエストはコントローラーに到達する前に差し止められます。

この投稿では、@IsMongoId() の基本的な使い方から内部実装、実践的な活用法、さらにカスタム検証まで全てを扱っていきます。

Class‑ValidatorとNestJSの出会い

NestJSはTypeScriptベースの強力なサーバーフレームワークです。その中で class‑validator はNestJS公式ドキュメントで推奨されている検証ライブラリであり、デコレーター形式の直感的な文法で入力データを検証できるようにしてくれます。

class‑validatorを導入すると、サービス層で逐一検証ロジックを書く必要がなくなります。DTOにデコレーターを追加するだけでリクエストデータの構造を制御できます。実際、多くの開発者が class‑validator を導入した後、製品の安定性と開発の利便性が大幅に向上したと語っています。

@IsMongoId()とは何か?

定義と目的

@IsMongoId()class‑validator が提供する組み込みデコレーターで、文字列がMongoDBのObjectId形式(24文字16進数)であるかを検証します。

MongoDBでは各ドキュメントが固有の _id フィールドを持っており、これは12バイトのBSONタイプObjectIdです。文字列として表すと24桁の16進数形式になりますが、@IsMongoId() はまさにこの形式を検証する役割を担っています。

基本使用例

import { IsMongoId, IsNotEmpty } from 'class-validator';

export class FindUserDto {
  @IsNotEmpty()
  @IsMongoId()
  userId: string;
}

上記の例では、userId は必ず「空ではなく」「MongoDB ObjectId形式」でなければなりません。もし誤った形式のIDが渡されると、次のようなエラー応答が返されます:

{
  "statusCode": 400,
  "message": ["userId must be a mongodb id"],
  "error": "Bad Request"
}

validator.js の活用

@IsMongoId() の内部実装を見てみると興味深い点があります:

import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import isMongoIdValidator from 'validator/lib/isMongoId';

export const IS_MONGO_ID = 'isMongoId';

export function isMongoId(value: unknown): boolean {
  return typeof value === 'string' && isMongoIdValidator(value);
}

export function IsMongoId(validationOptions?: ValidationOptions): PropertyDecorator {
  return ValidateBy(
    {
      name: IS_MONGO_ID,
      validator: {
        validate: (value): boolean => isMongoId(value),
        defaultMessage: buildMessage(
          eachPrefix => eachPrefix + '$property must be a mongodb id',
          validationOptions
        ),
      },
    },
    validationOptions
  );
}

ポイントは次のとおりです:

  • validator.js 依存validator/lib/isMongoId を利用して実際の検証を行っています。
  • 型制限:文字列 (string) 型のみを許可します。
  • 正規表現ベース:24文字16進数形式を正規表現で検証しています。
  • カスタマイズ可能validationOptions を通じてエラーメッセージを変更できます。

NestJSプロジェクトに Class‑Validator を設定する

インストール

まず必要なパッケージをインストールします:

npm i --save class-validator class-transformer

class‑transformer はプレーンなオブジェクトをクラスインスタンスへ変換するために必要なライブラリです。

ValidationPipe の適用

NestJSで class‑validator のデコレーターを動作させるには、ValidationPipe を使用する必要があります。グローバルに適用するか、特定のコントローラー/ルートにのみ適用できます。

グローバル適用 (main.ts):

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,
      forbidNonWhitelisted: true,
      transform: true,
    })
  );
  await app.listen(3000);
}
bootstrap();

コントローラー・レベル適用:

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';

@Controller('users')
export class UserController {
  @Post('find')
  @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
  async findUser(@Body() dto: FindUserDto) {
    return this.userService.findUserById(dto.userId);
  }
}

主なオプション:

  • whitelist: DTOに定義されていない属性を自動削除します。
  • forbidNonWhitelisted: DTOに存在しない属性が含まれていた場合、リクエストを拒否します。
  • transform: プレーンオブジェクトをDTOクラスインスタンスに自動変換します。

DTOで @IsMongoId() を活用する

実践例:ユーザー取得API

// user.dto.ts
import { IsMongoId, IsNotEmpty } from 'class-validator';

export class GetUserByIdDto {
  @IsNotEmpty({ message: 'userId는 필수입니다.' })
  @IsMongoId({ message: 'userId는 유효한 MongoDB ObjectId여야 합니다.' })
  userId: string;
}
// user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('find')
  async findUser(@Body() dto: GetUserByIdDto) {
    console.log(dto.userId);
    return this.userService.findUserById(dto.userId);
  }
}

有効なリクエスト:

{
  "userId": "507f191e810c19729de860ea"
}

無効なリクエスト:

{
  "userId": "12345"
}

レスポンス:

{
  "statusCode": 400,
  "message": ["userId는 유효한 MongoDB ObjectId여야 합니다."],
  "error": "Bad Request"
}

リクエストパラメーター検証

クエリパラメーターやURLパラメーターでも同様に使用できます:

import { Controller, Get, Param } from '@nestjs/common';
import { IsMongoId } from 'class-validator';

export class UserIdParam {
  @IsMongoId()
  id: string;
}

@Controller('users')
export class UserController {
  @Get(':id')
  async getUser(@Param() params: UserIdParam) {
    return this.userService.findById(params.id);
  }
}

よくある問題と解決策

空文字列の処理

@IsMongoId() は空文字列を検証しません。空文字列も拒否したい場合は @IsNotEmpty() を併用してください:

export class CreateNewsDto {
  @IsNotEmpty()
  @IsString()
  title: string;

  @IsNotEmpty()
  @IsString()
  content: string;

  @IsNotEmpty()
  @IsString()
  @IsMongoId()
  pageId: string;

  @IsOptional()
  @IsString()
  @IsMongoId()
  ownerId: string;
}

@IsOptional() を使用すれば、そのフィールドが存在しないか undefined の場合は検証をスキップできます。

カスタムエラーメッセージ

デフォルトのエラーメッセージが気に入らない場合はカスタマイズできます:

export class FindUserDto {
  @IsMongoId({ message: '올바른 사용자 ID 형식이 아닙니다.' })
  userId: string;
}

多言語対応が必要な場合は、i18nライブラリと組み合わせて使用することもできます。

高度な活用法とカスタマイズ

他の検証デコレーターと結合

複数のデコレーターを組み合わせて、より精緻な検証ルールを作ることができます:

import { IsMongoId, IsOptional, IsArray, ArrayMinSize } from 'class-validator';

export class AssignTasksDto {
  @IsArray()
  @ArrayMinSize(1)
  @IsMongoId({ each: true, message: '각 항목은 유효한 MongoDB ObjectId여야 합니다.' })
  userIds: string[];

  @IsMongoId()
  projectId: string;
}

{ each: true } オプションを使うと、配列の各要素を個別に検証できます。

カスタム検証器:DB存在確認

@IsMongoId() は形式のみ検証し、該当IDが実際にデータベースに存在するかどうかを確認しません。これを実現するためにカスタム検証器を作ってみましょう:

import { registerDecorator, ValidationOptions, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

@ValidatorConstraint({ name: 'IsExistingMongoId', async: true })
@Injectable()
export class IsExistingMongoIdConstraint implements ValidatorConstraintInterface {
  constructor(
    @InjectModel('User') private readonly userModel: Model<any>,
  ) {}

  async validate(value: string, args: ValidationArguments): Promise<boolean> {
    const modelName = args.constraints[0];
    
    if (modelName === 'User') {
      const user = await this.userModel.findById(value).exec();
      return !!user;
    }
    
    return false;
  }

  defaultMessage(args: ValidationArguments): string {
    return `${args.property}에 해당하는 문서가 존재하지 않습니다.`;
  }
}

export function IsExistingMongoId(
  modelName: string,
  validationOptions?: ValidationOptions,
): PropertyDecorator {
  return (object, propertyName) => {
    registerDecorator({
      name: 'IsExistingMongoId',
      target: object.constructor,
      propertyName,
      constraints: [modelName],
      options: validationOptions,
      validator: IsExistingMongoIdConstraint,
    });
  };
}

使用法:

export class UpdateUserDto {
  @IsExistingMongoId('User', { message: '존재하지 않는 사용자입니다.' })
  userId: string;
}

カスタム検証器は 非同期処理 をサポートしているため、データベースクエリを通じた検証が可能です。ただし、性能に影響を与える可能性があるので、必要な場合だけ使用してください。

@IsMongoId() でより安全なAPIを作る

@IsMongoId() は小さいですが強力なツールです。このデコレーターひとつで次のような利点を得られます:

  • コード簡素化:サービス層で繰り返される検証ロジックを削除できます。
  • 一貫性:すべてのMongoDB ObjectId検証が同じ方式で行われます。
  • エラーハンドリング:誤ったリクエストを事前にブロックして、不必要なデータベースクエリを防げます。
  • 保守性:DTOを見ただけでどんな検証が行われているかが明確になります。

NestJSプロジェクトでMongoDBを使うなら、@IsMongoId() は必須のツールです。基本の使い方からカスタム検証器まで幅広く使えるようにしておくと、より堅牢で安全なAPIを作成できることでしょう。

今すぐあなたのプロジェクトに class‑validator をインストールし、@IsMongoId() を適用してみてください。小さな変化が大きな違いを生み出します!