DEV Community

mnotr
mnotr

Posted on • Originally published at datacheck.dev

How to Validate International Phone Numbers in JavaScript (2026)

Phone number validation sounds simple until you go international. US numbers have 10 digits, UK mobiles have 11, German numbers range from 7 to 12, and India uses a completely different prefix system. A basic regex that works for one country breaks on all the others.

This guide covers four approaches, from simple to production-grade, with working code for each.

The Problem with Simple Regex

Most developers start here:

// DON'T use this in production
const isPhone = /^\d{10}$/.test(input);
// Misses: +44 7911 123456, 0049 170 1234567, +91 98765 43210
Enter fullscreen mode Exit fullscreen mode

This only matches exactly 10 digits. It rejects every valid international number, numbers with country codes, parentheses, dashes, or spaces. It also accepts invalid 10-digit strings that aren't real phone numbers.

Approach 1: Country-Specific Regex Patterns

Instead of one regex, use a separate pattern per country:

const patterns = {
  US: /^1?([2-9]\d{2}[2-9]\d{6})$/,
  GB: /^(?:44)?0?(7\d{9}|[1-9]\d{8,9})$/,
  DE: /^(?:49)?0?([1-9]\d{4,13})$/,
  FR: /^(?:33)?0?([1-9]\d{8})$/,
  IN: /^(?:91)?0?([6-9]\d{9})$/,
};

function validatePhone(digits, country) {
  const cleaned = digits.replace(/[\s\-\(\)\.\+]/g, '');
  const pattern = patterns[country];
  if (!pattern) return { valid: false, reason: 'Unsupported country' };
  return { valid: pattern.test(cleaned) };
}
Enter fullscreen mode Exit fullscreen mode

Pros: Zero dependencies, fast, works offline.
Cons: You need to maintain regex for every country. You also need the caller to specify the country.

Approach 2: Google's libphonenumber

Google maintains libphonenumber, originally built for Android. The JavaScript port is google-libphonenumber:

npm install google-libphonenumber
Enter fullscreen mode Exit fullscreen mode
const { PhoneNumberUtil, PhoneNumberFormat } = require('google-libphonenumber');
const phoneUtil = PhoneNumberUtil.getInstance();

function validate(input, countryCode) {
  try {
    const number = phoneUtil.parse(input, countryCode);
    return {
      valid: phoneUtil.isValidNumber(number),
      formatted: phoneUtil.format(number, PhoneNumberFormat.E164),
      type: phoneUtil.getNumberType(number),
      country: phoneUtil.getRegionCodeForNumber(number),
    };
  } catch (e) {
    return { valid: false, reason: e.message };
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros: Most accurate. Handles every country, number type, and format.
Cons: The bundle is ~1.2 MB. That's massive for a frontend app and significant for serverless functions where cold start time matters.

Approach 3: API-Based Validation

Offload the work to an API. This keeps your bundle size at zero and gets you extra data like location and line type:

async function validatePhone(input) {
  const res = await fetch(
    `https://datacheck.dev/api/validate?input=${encodeURIComponent(input)}&type=phone`
  );
  return res.json();
}

// Returns:
// {
//   valid: true,
//   formatted: "+1 4155551234",
//   country: "US",
//   details: {
//     type: "mobile",
//     area_code: "415",
//     location: "San Francisco, CA"
//   }
// }
Enter fullscreen mode Exit fullscreen mode

Or use the npm wrapper:

npm install datacheck-api
Enter fullscreen mode Exit fullscreen mode
import { validatePhone } from "datacheck-api";

const result = await validatePhone("+14155551234");
console.log(result.details.location); // "San Francisco, CA"
Enter fullscreen mode Exit fullscreen mode

Pros: Zero bundle size. Returns enriched data (location, line type). Always up to date.
Cons: Requires network call. Depends on external service uptime.

Approach 4: Hybrid (Recommended)

The production sweet spot: validate format client-side for instant feedback, then verify server-side via API before saving:

// Client-side: quick format check
function quickCheck(input) {
  const digits = input.replace(/\D/g, '');
  if (digits.length < 7 || digits.length > 15) {
    return { valid: false, reason: 'Phone must be 7-15 digits' };
  }
  return { valid: true };
}

// Server-side: full validation before saving
async function fullValidation(input) {
  const res = await fetch(
    `https://datacheck.dev/api/validate?input=${encodeURIComponent(input)}&type=phone`
  );
  const data = await res.json();
  if (!data.valid) throw new Error(data.reason);
  return data;
}
Enter fullscreen mode Exit fullscreen mode

This gives users instant feedback on typos while catching all edge cases on the backend.

Comparison Table

Approach Accuracy Bundle Size Countries Extra Data
Simple regex Low 0 KB 1 No
Country patterns Medium ~2 KB 5-30 No
libphonenumber High ~1.2 MB 250+ Type only
API (DataCheck) High 0 KB 30+ Type, location, area code
Hybrid High ~1 KB 30+ Type, location, area code

Common Pitfalls

1. Stripping the + sign too early
The + prefix indicates international format. If you strip it before parsing, +44 becomes 44, which could be confused with a domestic number starting with 4.

2. Assuming all countries have the same length
US numbers are always 10 digits. German numbers range from 7 to 12 digits. Chinese mobile numbers are 11 digits. Never hardcode length.

3. Ignoring leading zeros
In the UK, 07911 123456 is valid locally but the leading 0 is dropped in international format: +44 7911 123456. Your validator needs to handle both.

4. Not returning formatted output
Always store the E.164 formatted version (+14155551234). This is the universal format that SMS gateways, Twilio, and phone APIs expect.

Wrapping Up

For hobby projects, country-specific regex works fine. For production apps with international users, use either libphonenumber (if bundle size isn't a concern) or an API like DataCheck (if you want zero dependencies and enriched data).

The hybrid approach gives you the best of both: instant client-side feedback and accurate server-side validation with location data.

Top comments (0)