Validation Patterns
Common validation patterns for Nigerian phone numbers including form validation, OTP flows, and database storage.
These patterns cover common use cases for phone number validation in Nigerian applications.
Form Validation
Real-time Validation
Use isPossible for instant feedback as users type, then isValid on blur:
import { isPossible, isValid } from 'phoneng';
function PhoneInput() {
const [value, setValue] = useState('');
const [status, setStatus] = useState<'idle' | 'possible' | 'valid' | 'invalid'>('idle');
const handleChange = (input: string) => {
setValue(input);
if (!input.trim()) {
setStatus('idle');
return;
}
if (isPossible(input)) {
setStatus('possible');
} else {
setStatus('invalid');
}
};
const handleBlur = () => {
if (isValid(value)) {
setStatus('valid');
} else if (value.trim()) {
setStatus('invalid');
}
};
return (
<input
type="tel"
value={value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
className={status === 'invalid' ? 'error' : ''}
/>
);
}
Normalize Before Submit
Parse and normalize on form submission:
import { parse } from "phoneng";
async function handleSubmit(formData: FormData) {
const rawPhone = formData.get("phone") as string;
const result = parse(rawPhone);
if (!result.valid) {
throw new Error(`Invalid phone: ${result.reason}`);
}
await saveUser({
phone: result.e164,
phoneNetwork: result.network,
});
}
OTP Flow
Validate before sending OTP to avoid wasting credits:
import { parse } from "phoneng";
async function sendOTP(phone: string) {
const result = parse(phone);
if (!result.valid) {
return {
success: false,
error: "Invalid phone number",
code: result.reason,
};
}
const otp = generateOTP();
await smsGateway.send({
to: result.e164,
message: `Your OTP is ${otp}`,
});
await redis.setex(`otp:${result.e164}`, 300, otp);
return { success: true };
}
Database Storage
Always Store E.164
Store the canonical E.164 format in your database:
import { parse } from "phoneng";
async function createUser(data: { phone: string; name: string }) {
const result = parse(data.phone);
if (!result.valid) {
throw new Error("Invalid phone");
}
return await db.user.create({
data: {
name: data.name,
phone: result.e164,
phoneNetwork: result.network,
},
});
}
Display National Format
Convert back to national format when displaying to users:
function formatPhoneForDisplay(e164: string): string {
const result = parse(e164);
return result.valid ? result.national : e164;
}
function UserProfile({ user }) {
return (
<p>Phone: {formatPhoneForDisplay(user.phone)}</p>
);
}
Deduplication
Normalize before checking for duplicates:
import { parse } from "phoneng";
async function checkDuplicate(phone: string): Promise<boolean> {
const result = parse(phone);
if (!result.valid) {
return false;
}
const existing = await db.user.findUnique({
where: { phone: result.e164 },
});
return existing !== null;
}
Search
Pre-filter with isPossible
Use isPossible to quickly filter invalid searches:
import { isPossible, parse } from "phoneng";
async function searchByPhone(query: string) {
if (!isPossible(query)) {
return [];
}
const result = parse(query);
if (!result.valid) {
return [];
}
return await db.user.findMany({
where: { phone: result.e164 },
});
}
Search Multiple Formats
Search across different formats the user might enter:
import { parse } from "phoneng";
async function searchPhone(query: string) {
const result = parse(query);
if (!result.valid) {
return await db.user.findMany({
where: {
phone: { contains: query },
},
});
}
return await db.user.findMany({
where: { phone: result.e164 },
});
}
Bulk Import
CSV Processing
Validate and normalize a CSV of phone numbers:
import { parseMany } from "phoneng";
import { parse as parseCSV } from "csv-parse/sync";
interface ImportResult {
valid: Array<{ original: string; e164: string; network: string }>;
invalid: Array<{ original: string; reason: string }>;
}
function processCSV(csvContent: string): ImportResult {
const records = parseCSV(csvContent, { columns: true });
const phones = records.map((r) => r.phone);
const batch = parseMany(phones);
const valid = batch.results
.map((r, i) => ({ result: r, original: phones[i] }))
.filter((item) => item.result.valid)
.map((item) => ({
original: item.original,
e164: item.result.e164,
network: item.result.network,
}));
const invalid = batch.results
.map((r, i) => ({ result: r, original: phones[i] }))
.filter((item) => !item.result.valid)
.map((item) => ({
original: item.original,
reason: item.result.reason,
}));
return { valid, invalid };
}
Integration with Payment Providers
Paystack
Paystack requires the compact format without the plus sign:
import { parse } from "phoneng";
async function chargeCard(phone: string, amount: number) {
const result = parse(phone);
if (!result.valid) {
throw new Error("Invalid phone");
}
return await paystack.charge.create({
email: "user@example.com",
amount: amount * 100,
phone: result.compact,
});
}
Flutterwave
import { parse } from "phoneng";
async function initializePayment(phone: string, amount: number) {
const result = parse(phone);
if (!result.valid) {
throw new Error("Invalid phone");
}
return await flutterwave.charge({
phone_number: result.compact,
amount,
currency: "NGN",
});
}
Network-Specific Routing
Route operations based on detected network:
import { parse, type Network } from "phoneng";
const smsGateways: Record<Network, string> = {
MTN: "gateway-mtn",
AIRTEL: "gateway-airtel",
GLO: "gateway-glo",
NINE_MOBILE: "gateway-9mobile",
NTEL: "gateway-default",
VISAFONE: "gateway-default",
SMILE: "gateway-default",
MAFAB: "gateway-default",
UNKNOWN: "gateway-default",
};
async function sendSMS(phone: string, message: string) {
const result = parse(phone);
if (!result.valid) {
throw new Error("Invalid phone");
}
const gateway = smsGateways[result.network];
return await sendViaGateway(gateway, result.e164, message);
}