Back to Blog
Tutorial

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.

Payloadic Team9 min read

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:

  1. Visit Phone Validator API on RapidAPI
  2. Click "Subscribe to Test"
  3. Choose the free plan (100 requests/month)
  4. 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

  1. Debouncing: Already implemented (500ms delay)
  2. Caching: Add React Query or SWR for repeated validations
  3. Rate Limiting: Implement client-side throttling
  4. 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:

nextjsphone validationreacttypescriptapi integrationform validation

Ready to Try It Yourself?

Get started with Payloadic APIs and integrate phone validation or ZIP code lookup into your application today.