Introduction: The Nightmare of Handling International Phone Numbers
Any engineer who has worked on a product for international markets knows the feeling: phone number validation seems simple, but it's actually a bottomless pit. Countries have varying number lengths and convoluted formatting rules. Some countries even allow variable lengths (Indonesia is 10–13 digits). Just listing out the rules is enough to make your head spin. The traditional approach is to brute-force it with a pile of regular expressions (Regex), but as the number of supported countries grows, the Regex quickly balloons into a monster nobody dares to touch. This post shares our team's solution—standing directly on Google's shoulders and using the libphonenumber ecosystem to solve it for both frontend and backend in one go.
Why Maintaining Your Own Regex is a Road to Ruin
Extremely High Maintenance Costs
If you lay out the phone number specifications for various countries, you'll find this is definitely not a "write once and forget" task. Japanese mobile numbers are 11 digits, while landlines are 10. Malaysian mobile numbers are 10–11 digits. Indonesia is even more extreme, where anything from 10 to 13 digits is valid. Stuffing all these rules into Regex not only causes the line count to explode, but if a country changes its rules, making a precise change in that mess of Regex is as difficult as defusing a bomb.
Can't Keep Up with Global Changes
Global telecommunication regulations change more frequently than you might think. Between 2025–2026 alone, the UK updated its National Numbering Plan, Canada implemented Thousands-Block Pooling, and Spain blocked internationally spoofed domestic calls. If you rely on manually updating Regex for these changes, it's not only labor-intensive but also easy to miss updates, leading to validation failures in production—a user enters a perfectly valid number, but the system shows a red error message and blocks them.
Inability to Handle Hidden Details
The most classic example is the "leading zero" problem. Many countries have a local dialing convention that starts with a 0 (e.g., 0912-345-678 in Taiwan, 090-1234-5678 in Japan), but when converted to international format, this 0 must be removed (+886 912345678, +81 9012345678). If you build the input field yourself, users are often confused about "whether or not to include the 0." As a result, the backend receives a bunch of incorrectly formatted data, and downstream systems (like SMS gateways and KYC verification) all fail as a result.
The Industry Standard: Google's libphonenumber Ecosystem
The Core: Google's Commitment
libphonenumber is an open-source phone number handling library maintained by Google, covering the numbering rules for 218 countries/regions worldwide. The latest version is 9.0.32 (released 2026-06-05). Google has a dedicated team that tracks changes to national numbering plans from the ITU and updates the metadata every two weeks. This means as long as you keep your package version updated, Google is essentially shouldering the burden of keeping up with global telecom rules for you.
The Frontend Weapon: react-phone-number-input
In the React ecosystem, react-phone-number-input (v3.4.17) is currently the most popular international phone number input component, with over 2.05 million weekly downloads. Under the hood, it uses libphonenumber-js (v1.13.6), a pure JavaScript rewrite of the original Google library with a bundle size of just 145 KB (compared to the original's ~550 KB). It has native TypeScript support and is more than sufficient for the validation needs of most business applications.
💡
libphonenumber-jsdoes not support emergency numbers, short codes, vanity numbers (like1-800-GOT-MILK), or number geocoding, but these are rarely needed in typical user registration scenarios.
The Backend Guardian: libphonenumber-csharp
libphonenumber-csharp (v9.0.32) is a direct port of the original Google library by the C# community, with over 92.3 million total downloads on NuGet. It uses scheduled GitHub Actions to automatically sync updates from Google's upstream, with a delay of usually less than 1 day. It supports .NET 8.0 and .NET Standard 2.0.
⚠️ The official documentation specifically warns: If you don't keep the package updated,
IsValidNumbermight returnfalsefor new number formats, causing you to mistakenly block legitimate users.
Architecture Overview
By using packages from the same ecosystem on both the frontend and backend, we create a two-tiered validation architecture—the frontend is responsible for real-time UX feedback, and the backend is responsible for final data gatekeeping:
┌─────────────────────────────────────────────────────┐
│ 使用者瀏覽器 │
│ ┌───────────────────────────────────────────────┐ │
│ │ react-phone-number-input (v3.4.17) │ │
│ │ ├─ 國旗選擇 + 國碼自動帶入 │ │
│ │ ├─ 隨打隨格式化 │ │
│ │ ├─ 自動去零 (Leading Zero) │ │
│ │ └─ 底層:libphonenumber-js (v1.13.6, 145KB) │ │
│ └───────────────────────────┬───────────────────┘ │
│ │ E.164 格式 │
│ │ (+886912345678) │
└──────────────────────────────┼──────────────────────┘
▼
┌──────────────────────────────┼──────────────────────┐
│ API Server │
│ ┌───────────────────────────┴───────────────────┐ │
│ │ libphonenumber-csharp (v9.0.32) │ │
│ │ ├─ IsValidNumber() 嚴格驗證 │ │
│ │ ├─ 解析國碼 + 號碼類型 (手機/市話) │ │
│ │ └─ 自動同步 Google 上游 (GitHub Actions) │ │
│ └───────────────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ 資料庫 (E.164 格式儲存) │
└─────────────────────────────────────────────────────┘
Practical Architecture: Frontend Experience and Backend Validation
Frontend Implementation (React)
Using react-phone-number-input is very intuitive. A few lines of code can create an input field with country code selection, real-time formatting, and smart flag switching:
import PhoneInput, { isValidPhoneNumber } from 'react-phone-number-input'
import 'react-phone-number-input/style.css'
function PhoneForm() {
const [phone, setPhone] = useState<string | undefined>()
const handleSubmit = () => {
if (phone && isValidPhoneNumber(phone)) {
// phone 已經是 E.164 格式,例如 "+886912345678"
api.post('/register', { phone })
}
}
return (
<PhoneInput
international
defaultCountry="TW"
countries={['TW', 'JP', 'US', 'MY', 'ID', 'SG', 'TH']}
value={phone}
onChange={setPhone}
placeholder="輸入電話號碼"
/>
)
}
The UX improvements from this code are:
- ✅ As-you-type formatting: Entering a Japanese number automatically formats it as
090 1234 5678, which is clean and clear. - ✅ Smart paste detection: When a user pastes
+62 812 3456 7890, the country flag automatically switches to Indonesia 🇮🇩. - ✅ Automatic leading zero removal: After selecting Taiwan, if a user enters
0912345678, the component automatically converts it to+886 912345678. - ✅ Ability to restrict the country list: Use the
countriesprop to display only the countries your business serves.
📊 Live Demo: https://catamphetamine.github.io/react-phone-number-input/
Backend Validation (C#)
Frontend validation is primarily for UX; the backend is the final line of defense. Use libphonenumber-csharp for strict validation:
using PhoneNumbers;
public class PhoneValidator
{
private static readonly PhoneNumberUtil PhoneUtil = PhoneNumberUtil.GetInstance();
public static bool ValidatePhone(string phoneNumber)
{
try
{
// 解析號碼(第二個參數為預設國碼區域,傳 null 表示號碼必須包含 "+" 國碼)
var number = PhoneUtil.Parse(phoneNumber, null);
// 嚴格驗證:檢查號碼長度、格式是否符合該國規則
if (!PhoneUtil.IsValidNumber(number))
return false;
// 取得號碼類型(手機、市話、VOIP 等)
var numberType = PhoneUtil.GetNumberType(number);
// 可依業務需求限制只接受手機號碼
return numberType == PhoneNumberType.MOBILE
|| numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE;
}
catch (NumberParseException)
{
return false;
}
}
}
Best Practice: Store Everything in E.164 Format
Regardless of the format received from the frontend, always convert it to E.164 format (+國碼號碼, e.g., +886912345678) before storing it in the database. The benefits are:
| Aspect | Advantages of E.164 |
|---|---|
| Format Consistency | Any number from anywhere in the world has the same format, eliminating confusion over "with/without leading zero." |
| Downstream Compatibility | Services like Twilio and AWS SNS all require E.164. |
| Query Convenience | When checking for uniqueness in the database, you don't have to worry about multiple representations of the same number. |
| Length Constraint | A maximum of 15 digits (including country code), making field design straightforward. |
Business Considerations: Is It Free for Commercial Use?
| Package | License | Commercial Use | Closed Source | Viral (Copyleft) |
|---|---|---|---|---|
react-phone-number-input | MIT | ✅ Fully permitted | ✅ No need to disclose source code | ❌ No |
libphonenumber-js | MIT | ✅ Fully permitted | ✅ No need to disclose source code | ❌ No |
libphonenumber-csharp | Apache 2.0 | ✅ Fully permitted | ✅ No need to disclose source code | ❌ No |
In short, both MIT and Apache 2.0 licenses are very friendly to commercial and closed-source projects. You can freely use, modify, and distribute the code, and even package it within paid software for sale, without any of the "forced open-sourcing" viral risks associated with licenses like GPL. You can confidently integrate them into production products without worrying about licensing issues.
Conclusion: Say Goodbye to Maintenance Nightmares and Focus on Your Core Business
Let's review the core benefits of adopting the libphonenumber ecosystem:
- 🚀 Maintenance cost approaches zero: Google updates global telecom rules every two weeks; you just need to run
npm update/dotnet update. - 📊 Significant improvement in data quality: Real-time frontend formatting + strict backend validation prevents junk data from entering your database.
- ✅ Professional user experience: Country flag selection, as-you-type formatting, and smart paste detection make your forms look like they belong to a world-class product.
This isn't just a technical choice; it's a strategic decision. Offload the tedious, non-core tasks to Google and the open-source community, allowing your team to focus on the business logic that truly creates value. If your project still has a tangled mess of Regex for different countries, now is the perfect time to refactor.



























Comments