LocalityAPI
Get API Key

Building Address Forms

Create smart, validated address forms using Locality API for city and postal code lookups.

Overview

A well-designed address form improves user experience and data quality. This guide shows how to build an address form that validates cities and postal codes in real-time using the Locality API.

What you'll build

An address form with country selection, city autocomplete, postal code validation, and automatic city/state population from postal codes.

Form Structure

A typical address form needs these fields, in order:

  1. Country — Determines validation rules and available cities
  2. Postal Code — Can auto-fill city and state
  3. City — Validated against country
  4. State/Region — Auto-filled or selected
  5. Street Address — Free-form input

Implementation

'use client'

import { useState, useCallback } from 'react'

interface AddressData {
  country: string
  postalCode: string
  city: string
  state: string
  street: string
}

export function AddressForm() {
  const [address, setAddress] = useState<AddressData>({
    country: '',
    postalCode: '',
    city: '',
    state: '',
    street: ''
  })
  const [citySuggestions, setCitySuggestions] = useState([])
  const [loading, setLoading] = useState(false)

  // Fetch city suggestions when user types
  const handleCityInput = useCallback(async (query: string) => {
    if (query.length < 2 || !address.country) return
    
    const res = await fetch(
      `/api/locality/autocomplete?q=${encodeURIComponent(query)}&country=${address.country}`
    )
    const data = await res.json()
    setCitySuggestions(data.suggestions || [])
  }, [address.country])

  // Auto-fill city from postal code
  const handlePostalCode = useCallback(async (postalCode: string) => {
    setAddress(prev => ({ ...prev, postalCode }))
    
    if (postalCode.length >= 4 && address.country) {
      setLoading(true)
      try {
        const res = await fetch(
          `/api/locality/postal?code=${postalCode}&country=${address.country}`
        )
        const data = await res.json()
        
        if (data.city) {
          setAddress(prev => ({
            ...prev,
            city: data.city,
            state: data.state || ''
          }))
        }
      } catch (err) {
        console.error('Postal lookup failed:', err)
      } finally {
        setLoading(false)
      }
    }
  }, [address.country])

  return (
    <form className="space-y-4">
      {/* Country Select */}
      <select
        value={address.country}
        onChange={(e) => setAddress({ ...address, country: e.target.value })}
        className="w-full p-2 border rounded"
      >
        <option value="">Select Country</option>
        <option value="US">United States</option>
        <option value="GB">United Kingdom</option>
        <option value="AE">United Arab Emirates</option>
        {/* Add more countries */}
      </select>

      {/* Postal Code */}
      <input
        type="text"
        placeholder="Postal Code"
        value={address.postalCode}
        onChange={(e) => handlePostalCode(e.target.value)}
        className="w-full p-2 border rounded"
      />

      {/* City with Autocomplete */}
      <div className="relative">
        <input
          type="text"
          placeholder="City"
          value={address.city}
          onChange={(e) => {
            setAddress({ ...address, city: e.target.value })
            handleCityInput(e.target.value)
          }}
          className="w-full p-2 border rounded"
        />
        {citySuggestions.length > 0 && (
          <ul className="absolute w-full bg-white border rounded mt-1 shadow-lg z-10">
            {citySuggestions.map((s: any) => (
              <li
                key={s.id}
                onClick={() => {
                  setAddress({ ...address, city: s.name })
                  setCitySuggestions([])
                }}
                className="p-2 hover:bg-gray-100 cursor-pointer"
              >
                {s.name}
              </li>
            ))}
          </ul>
        )}
      </div>

      {/* State/Region */}
      <input
        type="text"
        placeholder="State/Region"
        value={address.state}
        onChange={(e) => setAddress({ ...address, state: e.target.value })}
        className="w-full p-2 border rounded"
      />

      {/* Street Address */}
      <input
        type="text"
        placeholder="Street Address"
        value={address.street}
        onChange={(e) => setAddress({ ...address, street: e.target.value })}
        className="w-full p-2 border rounded"
      />

      <button type="submit" className="w-full p-2 bg-blue-600 text-white rounded">
        Save Address
      </button>
    </form>
  )
}

Best Practices

  • Country first — Always collect country before city to filter suggestions properly.
  • Debounce autocomplete — Wait 200ms after typing stops before fetching suggestions.
  • Postal code auto-fill — Use postal lookup to pre-fill city and state, saving user effort.
  • Allow manual override — Let users edit auto-filled fields in case of errors.
  • Cache country list — Fetch the countries list once and cache it client-side.

Validation Tips

typescript
// Validate city exists in country
async function validateCity(city: string, country: string): Promise<boolean> {
  const res = await fetch(
    `https://api.localityapi.com/v1/search?q=${encodeURIComponent(city)}&country=${country}&exact=true`,
    { headers: { 'X-API-Key': process.env.LOCALITY_API_KEY! } }
  )
  const data = await res.json()
  return data.results && data.results.length > 0
}

// Validate postal code format by country
const postalPatterns: Record<string, RegExp> = {
  US: /^\d{5}(-\d{4})?$/,
  GB: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i,
  AE: /^\d{5,6}$/,
  DE: /^\d{5}$/,
}

function validatePostalCode(code: string, country: string): boolean {
  const pattern = postalPatterns[country]
  return pattern ? pattern.test(code) : true
}