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 it2. 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 andnew 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 needexperimentalDecoratorsin tsconfig.json, as NestJS projects have; phone.mjs is plain JavaScript, so setallowJstoo.