How to Validate Phone Numbers in Next.js - Complete Guide 2026
Learn how to implement phone number validation in Next.js applications using the Payloadic Phone Validator API. Includes code examples, TypeScript support, and best practices for production apps.
Phone number validation is critical for modern web applications. Whether you're building a user registration system, contact form, or e-commerce checkout, validating phone numbers prevents fake signups, reduces fraud, and ensures you can reach your users.
In this comprehensive guide, you'll learn how to implement robust phone number validation in Next.js using the Payloadic Phone Validator API.
Why Validate Phone Numbers?
Before diving into code, let's understand why phone validation matters:
- Prevent fake signups: Block disposable and invalid numbers
- Reduce fraud: Identify VoIP and temporary numbers
- Improve deliverability: Ensure SMS/2FA messages reach real numbers
- Data quality: Maintain clean contact databases
- International support: Handle phone numbers from 190+ countries
According to industry research, up to 20% of user-submitted phone numbers contain errors or are fake. Implementing validation can dramatically improve your data quality.
What You'll Build
By the end of this tutorial, you'll have:
- A reusable phone validation hook for Next.js
- Real-time validation with visual feedback
- TypeScript type safety
- Server-side validation for security
- Error handling and loading states
- Support for international numbers
Prerequisites
- Next.js 14+ (App Router or Pages Router)
- Basic React/TypeScript knowledge
- RapidAPI account (free tier available)
- 15 minutes of your time
Step 1: Get Your API Key
First, sign up for the Phone Validator API on RapidAPI:
- Visit Phone Validator API on RapidAPI
- Click "Subscribe to Test"
- Choose the free plan (100 requests/month)
- Copy your API key from the dashboard
Add your API key to .env.local:
# .env.local
RAPIDAPI_KEY=your_api_key_here
⚠️ Security Note: Never expose your API key in client-side code. Always use environment variables and server-side validation.
Step 2: Create a Server Action (App Router)
For Next.js 13+, we'll use Server Actions for secure API calls:
// app/actions/validate-phone.ts
'use server'
interface ValidationResponse {
success: boolean
valid: boolean
number?: {
international: string
national: string
e164: string
country_code: string
country_name: string
}
carrier?: {
name: string
type: 'mobile' | 'landline' | 'voip' | 'toll-free'
}
error?: string
}
export async function validatePhoneNumber(
phoneNumber: string
): Promise<ValidationResponse> {
try {
const response = await fetch(
`https://phone-validator-api.p.rapidapi.com/validate?phone=${encodeURIComponent(phoneNumber)}`,
{
method: 'GET',
headers: {
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY!,
'X-RapidAPI-Host': 'phone-validator-api.p.rapidapi.com',
},
}
)
if (!response.ok) {
return {
success: false,
valid: false,
error: 'Validation service unavailable',
}
}
const data = await response.json()
return {
success: true,
valid: data.valid,
number: data.number,
carrier: data.carrier,
}
} catch (error) {
console.error('Phone validation error:', error)
return {
success: false,
valid: false,
error: 'Failed to validate phone number',
}
}
}
Step 3: Create a Reusable Hook
Now create a custom hook for easy validation in your components:
// hooks/use-phone-validation.ts
'use client'
import { useState, useCallback } from 'react'
import { validatePhoneNumber } from '@/app/actions/validate-phone'
interface UsePhoneValidationReturn {
validate: (phone: string) => Promise<void>
isValidating: boolean
isValid: boolean | null
result: any
error: string | null
}
export function usePhoneValidation(): UsePhoneValidationReturn {
const [isValidating, setIsValidating] = useState(false)
const [isValid, setIsValid] = useState<boolean | null>(null)
const [result, setResult] = useState<any>(null)
const [error, setError] = useState<string | null>(null)
const validate = useCallback(async (phone: string) => {
if (!phone || phone.length < 10) {
setIsValid(false)
setError('Phone number too short')
return
}
setIsValidating(true)
setError(null)
try {
const response = await validatePhoneNumber(phone)
if (response.success) {
setIsValid(response.valid)
setResult(response)
setError(null)
} else {
setIsValid(false)
setError(response.error || 'Validation failed')
}
} catch (err) {
setIsValid(false)
setError('Network error')
} finally {
setIsValidating(false)
}
}, [])
return {
validate,
isValidating,
isValid,
result,
error,
}
}
Step 4: Build the Phone Input Component
Create a beautiful, user-friendly phone input with validation:
// components/phone-input.tsx
'use client'
import { useState, useEffect } from 'react'
import { usePhoneValidation } from '@/hooks/use-phone-validation'
import { Check, X, Loader2 } from 'lucide-react'
interface PhoneInputProps {
value: string
onChange: (value: string, isValid: boolean) => void
label?: string
placeholder?: string
required?: boolean
}
export function PhoneInput({
value,
onChange,
label = 'Phone Number',
placeholder = '+1 (555) 123-4567',
required = false,
}: PhoneInputProps) {
const [inputValue, setInputValue] = useState(value)
const [touched, setTouched] = useState(false)
const { validate, isValidating, isValid, result, error } = usePhoneValidation()
// Debounced validation
useEffect(() => {
if (!inputValue || !touched) return
const timer = setTimeout(() => {
validate(inputValue)
}, 500) // Wait 500ms after user stops typing
return () => clearTimeout(timer)
}, [inputValue, touched, validate])
// Notify parent of changes
useEffect(() => {
if (isValid !== null) {
onChange(inputValue, isValid)
}
}, [isValid, inputValue, onChange])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
if (!touched) setTouched(true)
}
const getStatusIcon = () => {
if (isValidating) {
return <Loader2 className="h-5 w-5 animate-spin text-gray-400" />
}
if (isValid === true) {
return <Check className="h-5 w-5 text-green-500" />
}
if (isValid === false && touched) {
return <X className="h-5 w-5 text-red-500" />
}
return null
}
return (
<div className="space-y-2">
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className="relative">
<input
id="phone"
type="tel"
value={inputValue}
onChange={handleChange}
onBlur={() => setTouched(true)}
placeholder={placeholder}
required={required}
className={`
w-full px-4 py-3 pr-12 rounded-lg border
focus:outline-none focus:ring-2 transition-colors
${
isValid === true
? 'border-green-500 focus:ring-green-200'
: isValid === false && touched
? 'border-red-500 focus:ring-red-200'
: 'border-gray-300 focus:ring-blue-200'
}
`}
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{getStatusIcon()}
</div>
</div>
{/* Validation feedback */}
{touched && !isValidating && (
<div className="text-sm">
{isValid && result && (
<div className="text-green-600 space-y-1">
<p>✓ Valid {result.carrier?.type} number</p>
<p className="text-xs text-gray-600">
{result.number?.international} ({result.number?.country_name})
</p>
</div>
)}
{isValid === false && (
<p className="text-red-600">
{error || 'Invalid phone number'}
</p>
)}
</div>
)}
</div>
)
}
Step 5: Use the Component in Your Form
Here's how to integrate it into a registration form:
// app/register/page.tsx
'use client'
import { useState } from 'react'
import { PhoneInput } from '@/components/phone-input'
export default function RegisterPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
phoneValid: false,
})
const handlePhoneChange = (value: string, isValid: boolean) => {
setFormData(prev => ({
...prev,
phone: value,
phoneValid: isValid,
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.phoneValid) {
alert('Please enter a valid phone number')
return
}
// Submit your form
console.log('Form data:', formData)
}
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Create Account</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-200"
required
/>
</div>
<PhoneInput
value={formData.phone}
onChange={handlePhoneChange}
required
/>
<button
type="submit"
disabled={!formData.phoneValid}
className="w-full py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
>
Create Account
</button>
</form>
</div>
)
}
Advanced: Blocking Specific Number Types
Want to block VoIP or landline numbers? Add custom validation logic:
// app/actions/validate-phone.ts (add this function)
export async function validateAndFilterPhone(
phoneNumber: string,
options: {
allowVoip?: boolean
allowLandline?: boolean
allowedCountries?: string[]
} = {}
): Promise<ValidationResponse & { blocked?: boolean; reason?: string }> {
const result = await validatePhoneNumber(phoneNumber)
if (!result.valid) {
return result
}
// Apply filters
if (!options.allowVoip && result.carrier?.type === 'voip') {
return {
...result,
valid: false,
blocked: true,
reason: 'VoIP numbers are not allowed',
}
}
if (!options.allowLandline && result.carrier?.type === 'landline') {
return {
...result,
valid: false,
blocked: true,
reason: 'Landline numbers are not allowed',
}
}
if (
options.allowedCountries &&
!options.allowedCountries.includes(result.number!.country_code)
) {
return {
...result,
valid: false,
blocked: true,
reason: `Numbers from ${result.number!.country_name} are not allowed`,
}
}
return result
}
Performance Optimization Tips
- Debouncing: Already implemented (500ms delay)
- Caching: Add React Query or SWR for repeated validations
- Rate Limiting: Implement client-side throttling
- Error Boundaries: Wrap components in error boundaries
Common Pitfalls to Avoid
❌ Don't validate on every keystroke - Use debouncing
❌ Don't expose API keys - Use server actions/API routes
❌ Don't trust client-side only - Always validate on server
❌ Don't block too aggressively - Consider use case before blocking VoIP
✅ Do use proper error handling
✅ Do provide clear user feedback
✅ Do test with international numbers
✅ Do validate on form submission
Testing Your Implementation
Test with these phone numbers:
- Valid US Mobile: +1 (415) 555-0123
- Invalid: 123
- International: +44 7911 123456 (UK)
- Landline: +1 (212) 555-0100
Production Checklist
Before deploying to production:
- [ ] API key stored in environment variables
- [ ] Server-side validation implemented
- [ ] Error boundaries in place
- [ ] Loading states handled
- [ ] Mobile responsiveness tested
- [ ] International numbers tested
- [ ] Rate limiting considered
- [ ] Analytics tracking added
Next Steps
Now that you have phone validation working, consider:
- SMS Verification: Use validated numbers for 2FA
- Database Integration: Store validated phone data
- Analytics: Track validation success rates
- A/B Testing: Test different validation UX approaches
Frequently Asked Questions
Q: How accurate is the validation?
A: The Payloadic Phone Validator API uses carrier data and number portability databases for high accuracy. It's correct for 95%+ of numbers.
Q: Can I validate numbers offline?
A: No, phone validation requires real-time carrier lookups. Consider implementing graceful fallbacks for offline scenarios.
Q: What's the rate limit?
A: The free tier includes 100 validations/month. Paid plans start at $9.99/month for 10,000 validations.
Q: Does it work with all countries?
A: Yes, the API supports 190+ countries and automatically detects country codes.
Conclusion
You've now implemented production-ready phone number validation in Next.js! This setup provides:
- ✅ Real-time validation feedback
- ✅ Security through server actions
- ✅ TypeScript type safety
- ✅ International number support
- ✅ Carrier detection and filtering
The complete code examples are available in our Cookbook. For more advanced use cases, check out our Phone Validator API documentation.
Ready to get started? Get your free API key on RapidAPI →
Have questions or need help? Reach out to our support team at support@payloadic.com
Tags:
Ready to Try It Yourself?
Get started with Payloadic APIs and integrate phone validation or ZIP code lookup into your application today.