class-validator's IsMongoId: Validating MongoDB ObjectId

class-validator's IsMongoId: Validating MongoDB ObjectId

D
dongAuthor
8 min read

When developing APIs with NestJS, you often need to validate parameters in the MongoDB ObjectId format. If an invalid ID format is passed, it results in a database query error and an unfriendly error message to the user.

Using the @IsMongoId() decorator from class-validator makes this easy to handle. Just add this decorator to a DTO, and MongoDB ObjectId validation happens automatically. Invalid requests are blocked before reaching the controller.

In this post, we’ll cover everything from basic usage of @IsMongoId() to its internal implementation, real-world usage, and even creating custom validations.

Class-Validator meets NestJS

NestJS is a powerful server-side framework based on TypeScript. Among its ecosystem, class-validator is a validation library recommended by the official NestJS docs. It provides intuitive, decorator-based syntax for validating input data.

By using class-validator, you eliminate the need to write repetitive validation logic in the service layer. Just adding decorators to DTOs lets you control the structure of incoming requests. In fact, many developers report that using class-validator significantly improved both stability and development productivity.

What is @IsMongoId()?

Definition and Purpose

@IsMongoId() is a built-in decorator provided by class-validator that checks whether a string is in the MongoDB ObjectId format (a 24-character hexadecimal string).

In MongoDB, each document has a unique _id field, which is a 12-byte BSON ObjectId. When expressed as a string, it’s a 24-character hexadecimal. The @IsMongoId() decorator validates this format.

Basic Example

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

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

In this example, userId must be a non-empty string in MongoDB ObjectId format. If an invalid ID is provided, an error response like the following is returned:

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

Under the Hood: validator.js

Looking at the internal implementation of @IsMongoId() reveals some interesting points:

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
  );
}

Key points:

  • Depends on validator.js: Uses validator/lib/isMongoId for actual validation.

  • String only: Only accepts string type inputs.

  • Regex-based: Uses regular expressions to validate the 24-character hex format.

  • Customizable: You can change error messages with validationOptions.

Setting up Class-Validator in NestJS

Installation

First, install the required packages:

npm i --save class-validator class-transformer

class-transformer is used to convert plain objects into class instances.

Applying ValidationPipe

To enable decorators from class-validator in NestJS, use ValidationPipe. It can be applied globally or per controller/route.

Global Application (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();

Controller-level Application:

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);
  }
}

Key options:

  • whitelist: Automatically strips properties not defined in the DTO.

  • forbidNonWhitelisted: Rejects requests with properties not defined in the DTO.

  • transform: Automatically transforms plain objects into DTO class instances.

Using @IsMongoId() in DTOs

Practical Example: User Lookup API

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

export class GetUserByIdDto {
  @IsNotEmpty({ message: 'userId is required.' })
  @IsMongoId({ message: 'userId must be a valid 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);
  }
}

Valid Request:

{
  "userId": "507f191e810c19729de860ea"
}

Invalid Request:

{
  "userId": "12345"
}

Response:

{
  "statusCode": 400,
  "message": ["userId must be a valid MongoDB ObjectId."],
  "error": "Bad Request"
}

Validating Request Parameters

You can also use it with query or URL parameters:

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);
  }
}

Common Issues and Solutions

Handling Empty Strings

@IsMongoId() does not validate empty strings. Use @IsNotEmpty() in combination to reject them:

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

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

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

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

Using @IsOptional() allows skipping validation if the field is missing or undefined.

Custom Error Messages

Don’t like the default error message? Customize it:

export class FindUserDto {
  @IsMongoId({ message: 'Invalid user ID format.' })
  userId: string;
}

If you need multilingual support, consider using an i18n library.

Advanced Use and Customization

Combining with Other Validators

You can combine multiple decorators for more sophisticated validation rules:

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

export class AssignTasksDto {
  @IsArray()
  @ArrayMinSize(1)
  @IsMongoId({ each: true, message: 'Each item must be a valid MongoDB ObjectId.' })
  userIds: string[];

  @IsMongoId()
  projectId: string;
}

Use { each: true } to validate each array element individually.

Custom Validator: Check if ID Exists in DB

@IsMongoId() only checks format, not existence in the database. Let’s create a custom validator for that:

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 `No document found for ${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,
    });
  };
}

Usage:

export class UpdateUserDto {
  @IsExistingMongoId('User', { message: 'User does not exist.' })
  userId: string;
}

Since custom validators support async operations, you can use database queries. Just be mindful of performance impacts—use this only when necessary.

Build Safer APIs with @IsMongoId()

@IsMongoId() is a small but powerful tool. With just one decorator, you gain:

  • Simplified code: Eliminates repetitive validation logic from service layers.

  • Consistency: All MongoDB ObjectId validations follow the same rule.

  • Error handling: Blocks invalid requests early, avoiding unnecessary DB queries.

  • Maintainability: Easy to understand what’s being validated just by looking at the DTO.

If you’re using MongoDB in a NestJS project, @IsMongoId() is a must. Master its basic usage and advanced features to build more robust and secure APIs.

Install class-validator and start using @IsMongoId() in your project today. A small step that makes a big difference!

class-validator's IsMongoId: Validating MongoDB ObjectId | devdong