Find ZIP codes within a radius in JavaScript
Build a browser search that lists nearby US ZIP codes with distances in miles and kilometers, using an adjustable radius.
On this page
Working example
HTML + JavaScriptBefore you start
You'll build a search form with a US ZIP code, a radius, and a miles/kilometers selector. The results will show nearby ZIP codes in distance order. Everything runs in one HTML file with plain JavaScript.
Create a Public key on the keys page. Add localhost to its allowed domains and add your production hostname before publishing the example. Browser code uses a parse_public_ key. A parse_ secret key belongs on your server.
Copy the complete example at the bottom of this page into index.html and replace YOUR_PUBLIC_API_KEY. From the directory containing the file, run:
python3 -m http.server 8000Open localhost:8000. A direct file:// page won't have the website origin the public key needs. Keep the page on localhost, matching the domain you added to the key.
1. Collect the search area
The form asks for an origin ZIP, a radius, and a unit. ZIP is a text input, which preserves leading zeroes. The radius is a number because it represents a measurement.
<form id="radius-form">
<div class="fields">
<label for="zip">ZIP code
<input id="zip" name="postal" type="text" inputmode="numeric" autocomplete="postal-code"
pattern="[0-9]{5}" maxlength="5" title="Enter a five-digit US ZIP code" value="28202" required>
</label>
<label for="radius">Radius
<input id="radius" name="radius" type="number" min="0.1" max="500" step="0.1" value="25" aria-describedby="radius-limit" required>
</label>
<label for="unit">Unit
<select id="unit" name="unit"><option value="mi">Miles</option><option value="km">km</option></select>
</label>
</div>
<button type="submit">Find nearby ZIPs</button>
<p id="status" role="status" aria-live="polite" aria-atomic="true">Enter a ZIP and choose how far to search.</p>
</form>Use a submit button so the visitor chooses when to run the search. Editing the radius from 10 to 25 should produce one search when they submit, without making requests for every intermediate keystroke.
The endpoint accepts radii from 0.1 to 800 kilometers or 0.1 to 500 miles. Keep the unit visible beside the radius. The same number describes a different search area after someone changes from miles to kilometers.
2. Request nearby ZIP codes
Call /postal/{code}/nearby with country=US, radius, and unit. The unit values are mi and km.
async function lookupNearby(code, searchRadius, searchUnit, signal) {
if (!API_KEY.startsWith('parse_public_')) {
throw new Error('Add your public API key and allow this hostname in its domain list.');
}
const url = new URL(`https://api.parseapi.com/postal/${encodeURIComponent(code)}/nearby`);
url.search = new URLSearchParams({ country: 'US', radius: searchRadius, unit: searchUnit, key: API_KEY });
const response = await fetch(url, { signal });
if (!response.ok) {
const messages = {
400: 'Check the ZIP and radius. Some ZIPs have no coordinates for a nearby search.',
401: 'Check your public API key.',
403: 'Check your public key, its allowed hostnames, and your account access.',
404: 'The starting ZIP was not found in the covered data. Try another ZIP.',
429: 'The request limit was reached. Try again later.'
};
throw new Error(messages[response.status] || 'The nearby search is unavailable. Try again shortly.');
}
return response.json();
}Use URLSearchParams to build the query string and encode the ZIP in the path. Country stays explicit because this is a US ZIP search. A bare five-digit code can occur in more than one country's postal system.
The response echoes the origin postal, the country, and the effective radius and unit. Its nearby array contains the matching codes and their distances. Use this endpoint for a radius search. The neighbors field on an ordinary postal lookup is a short surrounding-code list without this search control.
The example checks the HTTP status and displays a lookup error when the request fails. While a request is running, the displayed results must still belong to that request's ZIP and radius. Changing the inputs clears an old result so it cannot be mistaken for the new search.
3. Render the results as a table
Read the rows from nearby. Each row provides postal, city, distance in kilometers, and distance_mi in miles. Choose the distance field that matches the selected display unit.
function showNearby(data) {
if (!data || !Array.isArray(data.nearby) || !['mi', 'km'].includes(data.unit)
|| !Number.isFinite(data.radius) || typeof data.postal !== 'string') {
throw new Error('The search returned an unexpected response. Try again.');
}
const miles = data.unit === 'mi';
document.querySelector('#distance-heading').textContent = miles ? 'Miles' : 'km';
document.querySelector('#summary').textContent = `Within ${data.radius} ${data.unit} of ${data.postal}`;
rows.replaceChildren();
// The API returns nearest first and excludes the origin. Keep that order.
for (const item of data.nearby) {
const row = document.createElement('tr');
const distance = miles ? item.distance_mi : item.distance;
for (const value of [item.postal, item.city ?? 'Unavailable',
Number.isFinite(distance) ? distance.toFixed(1) : 'Unavailable']) {
const cell = document.createElement('td');
cell.textContent = value;
row.append(cell);
}
rows.append(row);
}
results.hidden = data.nearby.length === 0;
setStatus(data.nearby.length
? `${data.nearby.length} nearby ZIP${data.nearby.length === 1 ? '' : 's'} returned, nearest first. Maximum 100.`
: 'No other covered ZIPs with coordinates were found within this radius.');
}The response is ordered nearest first and excludes the origin ZIP. Keep the returned order when building the table. Use textContent for API values so a name is displayed as text instead of being interpreted as HTML.
A city name can be unavailable. Show an empty-value label for that cell and keep the ZIP and distance visible. If the array is empty, explain that no nearby codes were returned for this search. An empty search result is different from a failed request.
The response includes at most 100 nearby ZIP codes. If you receive 100 rows, show that limit with the results. There may be more codes inside the radius, so a row count of 100 should never become a claim that the radius contains exactly 100 ZIP codes.
4. Check changes and empty states
Run the example with its initial ZIP and radius, then try a smaller radius. Check that the result heading and distance column reflect the search you ran. Switch units and run it again to confirm that the request sends the selected unit.
Inspect the response in your browser's Network panel if you want to compare the table to the actual JSON. Try an incomplete ZIP and a radius outside the form's range, then check the error state with the browser offline. None of those cases should leave an old table presented as a fresh result.
How to use these distances
Distances are straight-line measurements between the available postal coordinates. They describe approximate postal locations, and both the origin and each returned code need coordinates. Some covered codes have names or other fields without a point and therefore cannot participate in this search.
Use this list for nearby postal areas. A store locator needs your own store locations, and a delivery boundary may require address-level measurements. See the Postal API reference for the lookup contract and coverage details.
Next steps
If you already have two ZIP codes, calculate the distance between them in one request. To fill location fields in a form, start with ZIP-code autofill. The Postal API brings lookup, distance, and nearby search together.
Complete example
One HTML file with the form, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.
View and copy the complete source
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ZIP codes within a radius</title>
<style>
:root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
}
:root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
:root[data-theme="light"] { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
main { max-width: 560px; margin: 0 auto; }
h1 { margin: 0 0 6px; font-size: clamp(23px, 5vw, 30px); font-weight: 600; letter-spacing: -.035em; }
p { margin: 0; }
.intro, .note { color: var(--muted); }
form { margin-top: 24px; }
.fields { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
label { display: block; font-size: 13px; font-weight: 550; }
input, select, button { font: inherit; }
input, select { width: 100%; min-width: 0; margin-top: 7px; min-height: 46px; padding: 11px 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
button { min-height: 46px; margin-top: 14px; padding: 11px 18px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
button:disabled { opacity: .55; cursor: wait; }
#status { min-height: 44px; margin-top: 14px; color: var(--muted); font-size: 13px; }
#status[data-error="true"] { color: var(--error); }
.results { max-height: 280px; overflow: auto; border-block: 1px solid var(--line); margin: 6px 0 18px; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 13px; }
caption { padding: 12px 8px; text-align: left; color: var(--muted); }
th, td { padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; }
th { position: sticky; top: 0; background: var(--bg); color: var(--muted); font-weight: 550; }
th:last-child, td:last-child { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.note { border-top: 1px solid var(--line); padding-top: 16px; font-size: 12px; }
@media (max-width: 380px) { body { padding: 18px; } .fields { grid-template-columns: 1fr 1fr; } .fields label:first-child { grid-column: 1 / -1; } button { width: 100%; } }
</style>
</head>
<body>
<main>
<h1>Start with a ZIP. Look around.</h1>
<p class="intro">Find nearby US ZIP codes, ordered by distance.</p>
<!-- example:form -->
<form id="radius-form">
<div class="fields">
<label for="zip">ZIP code
<input id="zip" name="postal" type="text" inputmode="numeric" autocomplete="postal-code"
pattern="[0-9]{5}" maxlength="5" title="Enter a five-digit US ZIP code" value="28202" required>
</label>
<label for="radius">Radius
<input id="radius" name="radius" type="number" min="0.1" max="500" step="0.1" value="25" aria-describedby="radius-limit" required>
</label>
<label for="unit">Unit
<select id="unit" name="unit"><option value="mi">Miles</option><option value="km">km</option></select>
</label>
</div>
<button type="submit">Find nearby ZIPs</button>
<p id="status" role="status" aria-live="polite" aria-atomic="true">Enter a ZIP and choose how far to search.</p>
</form>
<!-- /example:form -->
<div id="results" class="results" tabindex="0" role="region" aria-label="Nearby ZIP codes" hidden>
<table>
<caption id="summary"></caption>
<thead><tr><th scope="col">ZIP</th><th scope="col">City</th><th scope="col" id="distance-heading">Miles</th></tr></thead>
<tbody id="rows"></tbody>
</table>
</div>
<p class="note">Up to 100 nearby ZIPs, excluding the starting ZIP. Distances are straight lines between approximate postal coordinates. <span id="radius-limit">Radius: 0.1 to 500 miles.</span></p>
</main>
<script>
// Use a public key restricted to your development and production hostnames.
const API_KEY = 'YOUR_PUBLIC_API_KEY';
const form = document.querySelector('#radius-form');
const zip = document.querySelector('#zip');
const radius = document.querySelector('#radius');
const unit = document.querySelector('#unit');
const button = form.querySelector('button');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const rows = document.querySelector('#rows');
let controller;
let revision = 0;
function setStatus(message, error = false) {
status.textContent = message;
status.dataset.error = String(error);
}
function cancelRequest() {
revision += 1;
controller?.abort();
controller = undefined;
button.disabled = false;
form.setAttribute('aria-busy', 'false');
}
// example:request
async function lookupNearby(code, searchRadius, searchUnit, signal) {
if (!API_KEY.startsWith('parse_public_')) {
throw new Error('Add your public API key and allow this hostname in its domain list.');
}
const url = new URL(`https://api.parseapi.com/postal/${encodeURIComponent(code)}/nearby`);
url.search = new URLSearchParams({ country: 'US', radius: searchRadius, unit: searchUnit, key: API_KEY });
const response = await fetch(url, { signal });
if (!response.ok) {
const messages = {
400: 'Check the ZIP and radius. Some ZIPs have no coordinates for a nearby search.',
401: 'Check your public API key.',
403: 'Check your public key, its allowed hostnames, and your account access.',
404: 'The starting ZIP was not found in the covered data. Try another ZIP.',
429: 'The request limit was reached. Try again later.'
};
throw new Error(messages[response.status] || 'The nearby search is unavailable. Try again shortly.');
}
return response.json();
}
// /example:request
// example:result
function showNearby(data) {
if (!data || !Array.isArray(data.nearby) || !['mi', 'km'].includes(data.unit)
|| !Number.isFinite(data.radius) || typeof data.postal !== 'string') {
throw new Error('The search returned an unexpected response. Try again.');
}
const miles = data.unit === 'mi';
document.querySelector('#distance-heading').textContent = miles ? 'Miles' : 'km';
document.querySelector('#summary').textContent = `Within ${data.radius} ${data.unit} of ${data.postal}`;
rows.replaceChildren();
// The API returns nearest first and excludes the origin. Keep that order.
for (const item of data.nearby) {
const row = document.createElement('tr');
const distance = miles ? item.distance_mi : item.distance;
for (const value of [item.postal, item.city ?? 'Unavailable',
Number.isFinite(distance) ? distance.toFixed(1) : 'Unavailable']) {
const cell = document.createElement('td');
cell.textContent = value;
row.append(cell);
}
rows.append(row);
}
results.hidden = data.nearby.length === 0;
setStatus(data.nearby.length
? `${data.nearby.length} nearby ZIP${data.nearby.length === 1 ? '' : 's'} returned, nearest first. Maximum 100.`
: 'No other covered ZIPs with coordinates were found within this radius.');
}
// /example:result
form.addEventListener('input', () => {
cancelRequest();
results.hidden = true;
rows.replaceChildren();
radius.max = unit.value === 'mi' ? '500' : '800';
document.querySelector('#radius-limit').textContent = `Radius: 0.1 to ${radius.max} ${unit.value === 'mi' ? 'miles' : 'km'}.`;
setStatus('Search again to use the changed values.');
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form.reportValidity()) return;
cancelRequest();
results.hidden = true;
rows.replaceChildren();
const current = revision;
controller = new AbortController();
const request = controller;
let timedOut = false;
const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
button.disabled = true;
form.setAttribute('aria-busy', 'true');
setStatus('Finding nearby ZIP codes...');
try {
const data = await lookupNearby(zip.value, radius.value, unit.value, request.signal);
if (current !== revision) return;
showNearby(data);
} catch (error) {
if (current !== revision) return;
const message = timedOut ? 'The search timed out. Try again.'
: error instanceof TypeError ? 'Could not reach parseAPI. Check your connection and try again.'
: error instanceof SyntaxError ? 'The response could not be read. Try again.' : error.message;
setStatus(message, true);
} finally {
clearTimeout(timeout);
if (current === revision) cancelRequest();
}
});
</script>
</body>
</html>
Need a key? Create a public API key. For every field and parameter, see Postal API reference.
Build something else
- Autofill city and state from a ZIP code in JavaScript
- Calculate distance between ZIP codes in JavaScript