Validate postal codes and ZIP codes for 249 countries. TypeScript-first. Zero dependencies.
Prefer to try before installing?
Browse the full 249-country dataset, run single and batch validations, and edit live code right in the browser — no install required.
Powered by Google's libaddressinput — the same dataset behind Chromium, Android, and Google Pay address forms.
Contents
npm install postal-code-checkerimport { validatePostalCode, validatePostalCodes } from "postal-code-checker";
validatePostalCode("US", "90210"); // → true
validatePostalCode("CA", "k1a 0t6"); // → true (case + whitespace tolerant)
validatePostalCode("GBR", "SW1A 1AA"); // → true (alpha-3 works too)
validatePostalCodes("US", ["12345", "oops"]); // → [true, false]Works in React, Next.js, Vue, Svelte, Angular, Node.js, Deno, Bun, and plain browser JS — no framework assumptions.
- 🌍 249 countries — the full ISO 3166-1 list, sourced live from Google's
libaddressinput. Regenerated per release so the regexes never drift from upstream. - 📋 Batch API —
validatePostalCodes(country, codes[])returns an index-alignedboolean[]. Designed for CSV imports, bulk address uploads, and form arrays. - 🔤 Alpha-2 and alpha-3 both work — call with
"US"or"USA","GB"or"GBR". No branching at your call sites. - ✨ Forgiving input — case-insensitive and whitespace-tolerant.
"k1a 0t6"," K1A 0T6 ", and"K1A0T6"all validate equivalently where the country allows it. - 🪶 Zero runtime dependencies — one install, nothing else pulled in. Keeps
node_modulessmall and supply-chain surface minimal. - 🧷 TypeScript-first —
.d.tsbundled. No@types/*package to install. - 📦 Dual ESM + CommonJS — modern
importand legacyrequire()both work out of the box. - 🌐 Framework-agnostic — React, Next.js, Vue, Svelte, Angular, Node.js, Deno, Bun, plain browser JS.
Ecommerce checkout — validate ZIP before hitting your address API
import { validatePostalCode } from "postal-code-checker";
function onZipBlur(country: string, zip: string) {
if (!validatePostalCode(country, zip)) {
return "Please check your postal code.";
}
// safe to POST to /address-lookup
}React Hook Form — drop-in custom validator
import { useForm } from "react-hook-form";
import { validatePostalCode } from "postal-code-checker";
const { register } = useForm<{ country: string; postal: string }>();
<input
{...register("postal", {
validate: (value, { country }) =>
validatePostalCode(country, value) || "Invalid postal code",
})}
/>CSV / bulk import sanity check — batch API
import { validatePostalCodes } from "postal-code-checker";
const rows = await parseCsv("./addresses.csv");
const postals = rows.map((r) => r.postal);
const results = validatePostalCodes("US", postals);
const bad = rows.filter((_, i) => !results[i]);
console.log(`${bad.length} rows need review`);Batch calls avoid N regex compiles — one country lookup, N matches. Index-aligned output so you can zip results back to input rows.
Next.js server action — country-aware validation
"use server";
import { validatePostalCode } from "postal-code-checker";
export async function submitAddress(data: FormData) {
const country = String(data.get("country"));
const postal = String(data.get("postal"));
if (!validatePostalCode(country, postal)) {
return { error: "Invalid postal code for that country" };
}
// persist…
}Address autocomplete fallback — when no API is available
import { getCountryByCode } from "postal-code-checker";
const country = getCountryByCode("DE");
// country.examplePostalCodes → ["10115"] ← use as placeholder
// country.postalCodePatterns → anchored regex strings, safe to renderValidates a single postal code against a country. Accepts both alpha-2 and alpha-3 ISO 3166-1 codes. Input is trimmed and uppercased before matching, so "k1a 0t6", " K1A 0T6 ", and "K1A0T6" all validate equivalently where the country allows it.
Validates an array of postal codes against one country and returns an index-aligned array of results. Unique to this package — use it for CSV imports, bulk address uploads, and form arrays.
Returns the canonical form of a valid postal code (trimmed, uppercased), ready to store in a database. Returns null when the input doesn't match the country's pattern or the country has no postal code system. Accepts alpha-2 and alpha-3.
format("CA", "k1a 0t6"); // → "K1A 0T6"
format("US", " 12345 "); // → "12345"
format("US", "ABC12"); // → nullGiven a postal code with no country context, returns every country whose pattern accepts the input. Sorted alphabetically by countryName — ready to render as a picker. Countries with no postal code system are excluded.
guessCountries("K1A 0T6");
// → [{ countryName: "Canada", countryCode: "CA" }]
guessCountries("12345");
// → [{ countryName: "Algeria", ... }, { countryName: "Germany", ... }, ...]Returns the full country record (patterns, example codes, name, 2-letter code) or null if unknown. Accepts alpha-2 or alpha-3. postalCodePatterns is a string[] — most countries have one entry, some (e.g. GB with BFPO) have several.
Returns { countryName, countryCode }[] for every supported country, sorted alphabetically.
Registers a user-supplied country dataset in one call, typically at app boot. Each entry either replaces a built-in country (when the alpha-2 key matches) or adds a brand-new country. Every utility in the package honors the override immediately — no call-site changes needed.
import { configure, validatePostalCode } from "postal-code-checker";
configure({
countries: {
// Kosovo — not in ISO 3166-1, so not in the bundled dataset
XK: {
patterns: ["/^(?:[1-7]\\d{4})$/"],
example: ["10000", "20000"],
country: "Kosovo",
alpha3: "XKX",
},
},
});
validatePostalCode("XK", "10000"); // → true
validatePostalCode("XKX", "10000"); // → true (alpha-3 works)Replace semantics per country, idempotent across calls, and a fail-fast ConfigurationError on bad input. Full guide: docs/CONFIGURATION.md.
SSR / multi-tenant:
configure()is a module-level singleton and isn't designed for per-request overrides. If you need that, open an issue describing your use case.
Discards any active configure() override and restores the bundled defaults. Primary use cases: test teardown (afterEach(resetConfig)), scenario switching, HMR.
afterEach(() => {
resetConfig();
});Retained for backward compatibility; delegates to the top-level validatePostalCode. Kept functional in 2.x; scheduled for removal in 3.0. Prefer the top-level exports in new code.
type CountryCode = string; // ISO 3166-1 alpha-2 ("US") or alpha-3 ("USA")
type AnyCountryCode = CountryCode | (string & {});
// preserves autocomplete for known codes while accepting runtime-added ones
type Country = {
postalCodePatterns: string[]; // regex strings wrapped in slashes, e.g. "/^\\d{5}$/"
examplePostalCodes: string[];
isGenericRegex: boolean;
countryName: string;
countryCode: AnyCountryCode;
};
// Shape accepted by configure()
type PostalCodeConfig = {
countries: {
[countryCode: string]: {
patterns: string[]; // each entry wrapped in slashes, e.g. "/^\\d{5}$/"
example: string[];
country: string;
alpha3?: string; // optional 3-letter uppercase alpha-3 code
};
};
};Coming from a different postal-code library? See
docs/SWITCHING.md— covers the common one-line equivalents, batch API migration, and the argument-order gotcha that trips most swaps.
v2.0 switches to Google's libaddressinput dataset and renames the regex field on the Country record. The runtime API (validatePostalCode, validatePostalCodes, getCountryByCode, getAllCountries) is unchanged — but anything reading country.postalCodeRegex directly needs an update.
Before (v1.x):
const country = getCountryByCode("US");
const regex = new RegExp(country.postalCodeRegex.slice(1, -1));
regex.test("12345");After (v2.0):
const country = getCountryByCode("US");
const ok = country.postalCodePatterns.some((wrapped) =>
new RegExp(wrapped.slice(1, -1)).test("12345")
);If you were only calling validatePostalCode / validatePostalCodes, nothing changes — normalization and return types are identical.
Also note:
country.countryNamevalues follow Google's canonical spelling — e.g."United States"(was"United States of America"),"Russia"(was"Russian Federation"). Update any string pins in tests or UI copy.- A handful of previously-orphan alpha-3 codes (Åland, Martinique, Réunion, Puerto Rico, etc.) now resolve correctly through
getCountryByCode.
// Before
import { usePostalCodeValidation } from "postal-code-checker";
const { validatePostalCode } = usePostalCodeValidation();
// After
import { validatePostalCode } from "postal-code-checker";The usePostalCodeValidation name followed React's hook naming convention, which confused non-React users and tripped the react-hooks/rules-of-hooks lint rule even though it's not an actual hook. The new direct API works identically in React, Node.js, Vue, Angular, Svelte, or plain JavaScript.
usePostalCodeValidation still exists in 2.x and delegates to the new implementation. Scheduled for removal in 3.0.
configure()+resetConfig()— user-supplied country overrides and brand-new countries via a single-place config (2.1.0)format()— canonical storable form, ornullif invalid (2.1.0)guessCountries()— countries whose pattern accepts an input (2.1.0)- Swap data source to Google
libaddressinput(2.0.0) - Reproducible data pipeline —
sync:data+sync:checkguard against upstream drift (2.0.0) postalCodePatterns: string[]— support countries with multiple valid patterns (2.0.0)- Interactive demo site at sashiksu.github.io/postal-code-checker (2.0.0)
- Full ISO 3166-1 coverage (249 countries, zero generic fallbacks) (2.0.0)
- Batch validation (
validatePostalCodes) (1.1.0) - Case + whitespace tolerant input (1.1.0)
- ISO 3166-1 alpha-3 support (1.1.0)
- Full unit-test coverage of utility functions (1.1.0)
- Subdivision-level validation (Google's
sub_zipsprefix data) createValidator()factory for SSR / multi-tenant use cases (if demand shows up)- Removal of deprecated
usePostalCodeValidation(3.0.0)
Postal code patterns, country names, and example codes come from Google's libaddressinput project (Apache-2.0), fetched from https://chromium-i18n.appspot.com/ssl-aggregate-address/data/<CC>. The same dataset powers address forms in Chromium, Android, and Google Pay.
scripts/sync-postal-data.ts regenerates src/assets/index.ts from upstream; npm run sync:check runs in CI and in prepublishOnly to block releases whose on-disk data has drifted from the script's output.
See NOTICE for the upstream Apache-2.0 attribution.
Prior to v2.0.0, data was sourced from the European Central Bank (ECB), retrieved 4 Aug 2024.
Pull requests welcome. See CONTRIBUTING.md for local setup, the country-data workflow (upstream-first via libaddressinput), and maintainer sync steps.
Quick start:
- Branch from
master:git checkout -b feature/<short-desc>orbugfix/<short-desc>. - Add or update tests under
src/__tests__/for any behavior change. - Follow existing style —
npm run prettierandnpm run lintship configs. - Open a PR targeting
master.
Found a bug, missing country, or want to propose a feature? Use the structured issue templates. For security issues, see SECURITY.md.
