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:
- Country — Determines validation rules and available cities
- Postal Code — Can auto-fill city and state
- City — Validated against country
- State/Region — Auto-filled or selected
- 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
}