Skip to content

Validate phone numbers in NestJS with class-validator

Phone number validation in NestJS that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets NestJS's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the JavaScript guide; the rest is NestJS's own validation hook, a few lines long.

1. The file

Take phone.mjs from the JavaScript guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Node 20 or later, or any current browser; no dependency. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or null when no plan accepts it.

npm install class-validator   # a NestJS app with ValidationPipe already has it

2. The NestJS side

is-phone-number-in-plans.ts

The decorator: the language guide's parse, registered with class-validator.

import { registerDecorator, type ValidationOptions } from 'class-validator'
import { parse } from './phone.mjs'

/** A phone number readable in the plans of phone.mjs; national numbers are read in defaultRegion. */
export function IsPhoneNumberInPlans(defaultRegion?: string, options?: ValidationOptions) {
  return (object: object, propertyName: string) =>
    registerDecorator({
      name: 'isPhoneNumberInPlans',
      target: object.constructor,
      propertyName,
      constraints: [defaultRegion],
      options: { message: '$property is not a valid phone number', ...options },
      validator: {
        validate: (value: unknown) => typeof value === 'string' && parse(value, defaultRegion) !== null,
      },
    })
}

signup.dto.ts

A DTO that uses it.

import { IsNotEmpty } from 'class-validator'
import { IsPhoneNumberInPlans } from './is-phone-number-in-plans.js'

export class SignupDto {
  @IsNotEmpty()
  name!: string

  @IsPhoneNumberInPlans('GB')
  phone!: string
}

3. The tests

signup.dto.test.ts: The tests, with node:test on the compiled output.

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { validate } from 'class-validator'
import { SignupDto } from './signup.dto.js'

const signup = (phone: string) => Object.assign(new SignupDto(), { name: 'Ada', phone })

test('a national number in the default region', async () => {
  assert.deepEqual(await validate(signup('07400 123456')), [])
})

test('an international number', async () => {
  assert.deepEqual(await validate(signup('+1 201-555-0123')), [])
})

test('what no plan accepts fails on the phone', async () => {
  const [error] = await validate(signup('+44 7400 12345'))
  assert.equal(error.property, 'phone')
  assert.deepEqual(error.constraints, { isPhoneNumberInPlans: 'phone is not a valid phone number' })
})
npx tsc && node --test 'dist/*.test.js'

Run before publishing with class-validator 0.15.1, TypeScript 5.9.3 and the file generated for GB, US, FR, as 2026-10-25.

What is specific to NestJS

  • With app.useGlobalPipes(new ValidationPipe()), NestJS validates every DTO a controller receives and answers 400 with the messages.
  • To receive the E.164 form, add class-transformer's @Transform(({ value }) => parse(value, 'GB')?.e164 ?? value) to the property and new ValidationPipe({ transform: true }): the transform runs before validation, and an E.164 value still passes.
  • class-validator also has its own IsPhoneNumber; this one reads the plans in phone.mjs, the same file the browser can use. The decorators need experimentalDecorators in tsconfig.json, as NestJS projects have; phone.mjs is plain JavaScript, so set allowJs too.