Regex Isn't Enough: How to Actually Validate Postal Codes
Every developer building an address form eventually reaches for regex. It's fast, it's client-side, and it catches obvious garbage input. But regex has a blind spot that trips up production systems: it validates format, not existence.
The Problem
Take US ZIP codes. The standard regex is simple:
^\d{5}(-\d{4})?$
This accepts 99999 -- five digits, valid format. But 99999 is not a real ZIP code. No mail is delivered there. Your checkout form says "looks good," and you ship to nowhere.
The same problem exists globally. SW9Z matches the UK postcode pattern but isn't a real outcode. 99999 matches the German PLZ pattern ^\d{5}$ but doesn't exist.
The Two-Layer Approach
For production systems -- e-commerce checkout, address forms, shipping calculators -- the practical approach is two layers.
Layer 1: Client-Side Format Check
Fast, offline, catches garbage input before it hits your server:
// Loose worldwide pattern -- catches obvious junk
const postalCodePattern = /^[a-z0-9][a-z0-9\- ]{0,10}[a-z0-9]$/i;
if (!postalCodePattern.test(userInput)) {
showError("Please enter a valid postal code");
}
For per-country format patterns, the Unicode CLDR maintains regex patterns for 158 countries. Use those for stricter client-side validation.
This layer saves API calls. If the input is obviously wrong, there's no reason to check whether it exists.
Layer 2: Server-Side Existence Check
Confirms the code is real. This is the layer regex can't replace:
# 99999 passes regex but doesn't exist
curl -s -X POST https://postaldatapi.com/api/validate \
-H "Content-Type: application/json" \
-d '{"zipcode": "99999", "apiKey": "YOUR_KEY"}'
# {"valid": false, ...}
# 90210 passes regex AND exists
curl -s -X POST https://postaldatapi.com/api/validate \
-H "Content-Type: application/json" \
-d '{"zipcode": "90210", "apiKey": "YOUR_KEY"}'
# {"valid": true, ...}
Same endpoint works for any country -- just add the country parameter:
# Valid UK postcode
curl -s -X POST https://postaldatapi.com/api/validate \
-H "Content-Type: application/json" \
-d '{"zipcode": "SW1A", "country": "GB", "apiKey": "YOUR_KEY"}'
# {"valid": true, ...}
# Invalid UK postcode (correct format, doesn't exist)
curl -s -X POST https://postaldatapi.com/api/validate \
-H "Content-Type: application/json" \
-d '{"zipcode": "SW9Z", "country": "GB", "apiKey": "YOUR_KEY"}'
# {"valid": false, ...}
With the Python SDK
from postaldatapi import PostalDataPI
client = PostalDataPI(api_key="YOUR_KEY")
# Check if a code exists
result = client.validate("99999")
print(result.valid) # False
result = client.validate("90210")
print(result.valid) # True
# Works globally
gb = client.validate("SW1A", country="GB")
print(gb.valid) # True
Why Not Just Use Google Maps?
Google's Geocoding API resolves addresses, not postal codes specifically. It works most of the time, but edge cases are real. For example, these six German postal codes all fail with Google's Geocoding API but resolve correctly with a dedicated postal code lookup:
| PLZ | City | Google Geocoding | Dedicated Lookup |
|-----|------|-----------------|-----------------|
| 13627 | Berlin | No result | Berlin |
| 21129 | Hamburg Waltershof | No result | Hamburg Waltershof |
| 14656 | Brieselang | No result | Brieselang |
| 85057 | Ingolstadt | No result | Ingolstadt |
| 74172 | Neckarsulm | No result | Neckarsulm |
| 30559 | Hannover | No result | Hannover |
The issue is that geocoding APIs resolve geographic areas, not postal code records. District boundary changes, merged municipalities, and sub-district naming can all cause lookups to fail silently.
The Bottom Line
Regex handles format. An API handles existence. For production, you want both:
The first layer saves you money. The second layer saves your users from shipping to addresses that don't exist.
PostalDataPI covers 70+ countries with sub-10ms responses. Sign up free -- 1,000 queries included, no credit card required.