Validate phone numbers with Joi
Phone number validation in Joi that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Joi'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 Joi'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 joi2. The Joi side
phone-schema.mjs
The rule: a custom validation that converts to E.164.
import Joi from 'joi'
import { parse } from './phone.mjs'
/**
* A phone number as people type it, read in the plans of phone.mjs and converted to its E.164 form.
* defaultRegion (an ISO code such as 'GB') reads numbers typed without their country code.
*/
export const phoneNumber = (defaultRegion) =>
Joi.string()
.trim()
.custom((text, helpers) => parse(text, defaultRegion)?.e164 ?? helpers.error('phone.invalid'))
.messages({ 'phone.invalid': '{{#label}} is not a valid phone number' })
export const Signup = Joi.object({
name: Joi.string().required(),
phone: phoneNumber('GB').required(),
})3. The tests
phone-schema.test.mjs: The tests, with node:test.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { Signup } from './phone-schema.mjs'
test('converts a national number in the default region to E.164', () => {
const { value, error } = Signup.validate({ name: 'Ada', phone: '07400 123456' })
assert.equal(error, undefined)
assert.equal(value.phone, '+447400123456')
})
test('converts an international number whatever the default region', () => {
assert.equal(Signup.validate({ name: 'Ada', phone: '+1 201-555-0123' }).value.phone, '+12015550123')
})
test('reports what no plan accepts, on the phone field', () => {
const { error } = Signup.validate({ name: 'Ada', phone: '+44 7400 12345' })
assert.deepEqual(error.details[0].path, ['phone'])
assert.equal(error.details[0].message, '"phone" is not a valid phone number')
})node --testRun before publishing with joi 18.3.0 and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Joi
- A
customfunction returns the new value or an error fromhelpers.error; returning the E.164 form converts the field, which is Joi's default (convert: true). {{#label}}is the field's label, quoted by Joi:"phone". Set.label('Phone number')on the field for a friendlier message.- In Express, validate
req.bodywithSignup.validate(req.body)and answer 400 witherror.detailswhen it fails.