Calculate distance between ZIP codes in JavaScript
Build a browser calculator for straight-line distance between two US ZIP codes, with results in miles and kilometers.
On this page
Working example
HTML + JavaScriptBefore you start
You'll build a calculator with two US ZIP inputs and a choice of miles or kilometers. One API request returns both measurements and identifies the two places. The complete example is a single HTML file with plain JavaScript.
Create a Public key on the keys page and add localhost to its allowed domains. Add your production hostname when you publish the calculator. Use the parse_public_ key in this browser example and keep parse_ secret keys on your server.
Copy the complete example at the bottom of this page into index.html, then replace YOUR_PUBLIC_API_KEY with your public key. Open a terminal in that file's directory and run:
python3 -m http.server 8000Visit localhost:8000. Opening the HTML directly with file:// won't provide the website origin the public key needs. The hostname in the address bar should match localhost on the key's allowed domain list.
1. Create two ZIP inputs
Label the inputs so the visitor can distinguish the starting ZIP from the destination. Use text fields for both, even though this calculator accepts only five-digit US ZIPs.
<form id="distance-form">
<div class="fields">
<label for="from">From ZIP
<input id="from" name="from" type="text" inputmode="numeric" pattern="[0-9]{5}" maxlength="5"
title="Enter a five-digit US ZIP code" value="28202" required>
</label>
<label for="to">To ZIP
<input id="to" name="to" type="text" inputmode="numeric" pattern="[0-9]{5}" maxlength="5"
title="Enter a five-digit US ZIP code" value="10001" required>
</label>
<label for="unit">Show in
<select id="unit" name="unit"><option value="mi">Miles</option><option value="km">km</option></select>
</label>
</div>
<button type="submit">Calculate distance</button>
<p id="status" role="status" aria-live="polite" aria-atomic="true">Choose two ZIPs, then calculate.</p>
</form>Keeping the inputs as strings preserves leading zeroes. inputmode="numeric" gives mobile users a convenient keyboard without changing the value into a number. A five-digit format check helps catch incomplete input before it makes a request, but only the lookup can tell you whether a code is available.
Use a submit button to calculate the distance. That gives the visitor time to enter both ZIPs and makes the action clear. A single status message beside the form can report loading or a failed lookup without replacing either input.
2. Make the distance request
Call /postal/{code}/distance/{other}?country=US. The first path value is the starting ZIP, and the second is the destination. Encode each value as a path segment before sending it.
async function lookupDistance(start, end, 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(start)}/distance/${encodeURIComponent(end)}`);
url.search = new URLSearchParams({ country: 'US', key: API_KEY });
const response = await fetch(url, { signal });
if (!response.ok) {
const messages = {
400: 'Check both ZIPs. Some ZIPs have no coordinates for a distance calculation.',
401: 'Check your public API key.',
403: 'Check your public key, its allowed hostnames, and your account access.',
404: 'One or both ZIPs were not found in the covered data. Try another pair.',
429: 'The request limit was reached. Try again later.'
};
throw new Error(messages[response.status] || 'The distance lookup is unavailable. Try again shortly.');
}
return response.json();
}The country scopes both ZIPs in this request. This calculator always sends US, so a five-digit value shared with another country's postal system stays unambiguous. The endpoint answers a distance question within one country.
The response includes from and to objects, each with a postal code and an available city name. distance is the straight-line distance in kilometers, and distance_mi is the same measurement in miles. The selector chooses the main result, with the other unit shown below it. Both are already in the response, so the request needs no unit parameter or browser-side conversion.
Check the response status before reading those fields as a successful result. An unknown code and a code without coordinates need an error message, not a result with zero miles. The complete example handles a failed request while keeping the entered ZIPs available to edit.
3. Display the places and distance
Render the response's from and to values with the measurement. Those values identify which lookup produced the result, including when the API normalizes a code.
function showDistance(data, displayUnit) {
if (!data || !data.from || !data.to) {
throw new Error('The lookup returned an unexpected response. Try again.');
}
// Both units are returned. Never coerce a missing value into zero.
if (!Number.isFinite(data.distance) || !Number.isFinite(data.distance_mi)) {
throw new Error('Distance is unavailable for these ZIPs. Try another pair.');
}
const label = (place) => place.city ? `${place.postal} (${place.city})` : place.postal;
document.querySelector('#route').textContent = `${label(data.from)} to ${label(data.to)}`;
const miles = displayUnit === 'mi';
const primary = miles ? data.distance_mi : data.distance;
const secondary = miles ? data.distance : data.distance_mi;
const formatted = primary.toLocaleString('en-US', { maximumFractionDigits: 2 });
document.querySelector('#distance').textContent = `${formatted} ${miles ? 'miles' : 'km'}`;
document.querySelector('#other-unit').textContent = `${secondary.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${miles ? 'km' : 'miles'} in a straight line`;
result.hidden = false;
setStatus(`Distance calculated: ${formatted} ${miles ? 'miles' : 'kilometers'} in a straight line.`);
}Treat zero as a real measurement. Looking up the same ZIP at both ends can return zero, so a truthiness check such as if (distance) would hide a valid result. Keep missing values distinct from numeric zero.
A city label may be unavailable even when a postal code has coordinates. The ZIP still identifies the place, so display it independently of the optional city name. Use textContent for the returned values when adding them to the page.
Clear the old result when either ZIP changes. If an earlier request is still running, it must not repaint that result after the visitor has entered a new pair. The complete example keeps request state with the inputs and ignores superseded results.
4. Check the calculator
Run the calculator with the initial ZIPs. Compare the displayed distance with the appropriate field in your browser's Network panel, then select the other unit and calculate again. Swap the inputs and calculate again. The place labels should swap, while the straight-line measurement in that unit stays the same.
Use the same ZIP in both fields to check the zero-distance case. Try an incomplete ZIP and confirm the form asks for five digits before submitting. Finally, take the browser offline and run a lookup. You should get a useful failure message, with the entered values still in place and no stale measurement shown as a new answer.
What the measurement means
The distance connects the available postal coordinates in a straight line. It describes approximate postal areas. Road mileage, travel time, and distances between particular street addresses require different measurements.
Both codes need coordinates. Some covered postal records have names or other information without a point, so a successful ordinary postal lookup does not guarantee a distance result. The Postal API reference documents this behavior alongside the response fields.
Next steps
For an open-ended search around one ZIP, find nearby ZIP codes within a radius. For an address form, autofill city and state. The Postal API has live examples of all three operations.
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>Distance between two ZIP codes</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, 1fr) minmax(0, 1fr) minmax(0, .8fr); 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); }
#result { border-top: 1px solid var(--line); padding: 20px 0; }
.distance { font-size: clamp(30px, 7vw, 46px); font-weight: 550; letter-spacing: -.045em; color: var(--accent); }
#route, #other-unit { color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
#route { margin-bottom: 8px; }
.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:last-child { grid-column: 1 / -1; } button { width: 100%; } }
</style>
</head>
<body>
<main>
<h1>Two ZIPs. One distance.</h1>
<p class="intro">Measure the straight-line distance between US postal areas.</p>
<!-- example:form -->
<form id="distance-form">
<div class="fields">
<label for="from">From ZIP
<input id="from" name="from" type="text" inputmode="numeric" pattern="[0-9]{5}" maxlength="5"
title="Enter a five-digit US ZIP code" value="28202" required>
</label>
<label for="to">To ZIP
<input id="to" name="to" type="text" inputmode="numeric" pattern="[0-9]{5}" maxlength="5"
title="Enter a five-digit US ZIP code" value="10001" required>
</label>
<label for="unit">Show in
<select id="unit" name="unit"><option value="mi">Miles</option><option value="km">km</option></select>
</label>
</div>
<button type="submit">Calculate distance</button>
<p id="status" role="status" aria-live="polite" aria-atomic="true">Choose two ZIPs, then calculate.</p>
</form>
<!-- /example:form -->
<div id="result" hidden>
<p id="route"></p>
<p id="distance" class="distance"></p>
<p id="other-unit"></p>
</div>
<p class="note">Straight-line distance between approximate postal coordinates. This is not driving distance, travel time, or the distance between street addresses.</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('#distance-form');
const from = document.querySelector('#from');
const to = document.querySelector('#to');
const unit = document.querySelector('#unit');
const button = form.querySelector('button');
const status = document.querySelector('#status');
const result = document.querySelector('#result');
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 lookupDistance(start, end, 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(start)}/distance/${encodeURIComponent(end)}`);
url.search = new URLSearchParams({ country: 'US', key: API_KEY });
const response = await fetch(url, { signal });
if (!response.ok) {
const messages = {
400: 'Check both ZIPs. Some ZIPs have no coordinates for a distance calculation.',
401: 'Check your public API key.',
403: 'Check your public key, its allowed hostnames, and your account access.',
404: 'One or both ZIPs were not found in the covered data. Try another pair.',
429: 'The request limit was reached. Try again later.'
};
throw new Error(messages[response.status] || 'The distance lookup is unavailable. Try again shortly.');
}
return response.json();
}
// /example:request
// example:result
function showDistance(data, displayUnit) {
if (!data || !data.from || !data.to) {
throw new Error('The lookup returned an unexpected response. Try again.');
}
// Both units are returned. Never coerce a missing value into zero.
if (!Number.isFinite(data.distance) || !Number.isFinite(data.distance_mi)) {
throw new Error('Distance is unavailable for these ZIPs. Try another pair.');
}
const label = (place) => place.city ? `${place.postal} (${place.city})` : place.postal;
document.querySelector('#route').textContent = `${label(data.from)} to ${label(data.to)}`;
const miles = displayUnit === 'mi';
const primary = miles ? data.distance_mi : data.distance;
const secondary = miles ? data.distance : data.distance_mi;
const formatted = primary.toLocaleString('en-US', { maximumFractionDigits: 2 });
document.querySelector('#distance').textContent = `${formatted} ${miles ? 'miles' : 'km'}`;
document.querySelector('#other-unit').textContent = `${secondary.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${miles ? 'km' : 'miles'} in a straight line`;
result.hidden = false;
setStatus(`Distance calculated: ${formatted} ${miles ? 'miles' : 'kilometers'} in a straight line.`);
}
// /example:result
form.addEventListener('input', () => {
cancelRequest();
result.hidden = true;
setStatus('Calculate again to use the changed values.');
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form.reportValidity()) return;
cancelRequest();
result.hidden = true;
const current = revision;
const displayUnit = unit.value;
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('Calculating the distance...');
try {
const data = await lookupDistance(from.value, to.value, request.signal);
if (current !== revision) return;
showDistance(data, displayUnit);
} catch (error) {
if (current !== revision) return;
const message = timedOut ? 'The lookup 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.