This tutorial guides developers building applications that integrate with PawFinder. Developers write or change JavaScript code in their own applications. This integration doesn’t require enabling a feature in the PawFinder service itself.
Use client-side location-based filtering to help potential adopters discover shelters near their current location. Learn how to integrate geolocation data with the PawFinder Service API and calculate distances to filter results dynamically.
The PawFinder Service is a REST API that provides shelter data with address information, but location-aware filtering happens on the client side. This tutorial demonstrates how to retrieve shelter data from the API, access the adopter’s geolocation, calculate distances, and filter shelters based on proximity.
The separation of concerns is key: the API serves as a data source, while client-side logic handles geolocation, geocoding, and distance calculations. This approach keeps the API stateless and allows adopters to perform location-based searches without more server processing.
PawFinder uses json-server to provide shelter data. The diagram below
shows what a production location-aware search implementation might look
like, orchestrating calls between a client app, the PawFinder API, the
browser Geolocation API, and an external geocoding service:
sequenceDiagram
actor Adopter as Adopter
participant App as Client App
participant Geo as Browser<br/>Geolocation API
participant API as PawFinder API
participant Geocoder as Geocoding API<br/>Google Maps
participant Calc as Distance<br/>Calculation
Adopter->>App: Click "Find Nearby Shelters"
App->>Geo: Request user location
Geo->>Adopter: Browser permission prompt
Adopter-->>Geo: Grant access
Geo-->>App: Return latitude, longitude
App->>API: GET /shelters
API-->>App: Shelter list with addresses
loop For each shelter address
App->>Geocoder: Geocode address
Geocoder-->>App: Latitude, longitude
end
App->>Calc: Calculate distances
Calc-->>App: Shelters sorted by distance
App-->>Adopter: Display nearby shelters<br/>on map or in list
Complete all appropriate steps in the Installation Guide before continuing this tutorial. This content also assumes familiarity with the following concepts:
GET requests
async/await patterns
Web applications leverage standard web platform APIs such as the Geolocation API and Fetch API to access location data and retrieve shelter information.
Mobile applications integrate native geolocation libraries, such as Core Location on iOS or Google Play Services on Android to provide location data.
Location-aware search follows this workflow:
GET /shelters# Run from the pawfinder-service root directory
npm start
Review Find the Perfect Pet for an alternative startup method.
Use cURL commands or the Postman desktop app to make requests. For detailed Postman setup steps, visit the Installation Guide.
Use cURL
# Retrieve all shelter profiles
# -X GET is optional, as GET is the default operation
# Recommended base_url = http://localhost:3000
curl -X GET "{base_url}/shelters" \
-H "Content-Type: application/json"
Use Postman desktop app
Set up a GET request to {base_url}/shelters
Response 200 OK - returns an array of shelter profile
objects with location data:
[
{
"name": "Dallas Animal Services",
"address": "1818 N Westmoreland Rd, Dallas, TX 75212",
"phone": "+1-214-671-0249",
"email": "info@dallasanimalservices.org",
"hours": "Mon-Sat 11:00-18:00",
"available_pet_count": 22,
"adoption_fee_range": "75-200",
"id": 1,
},
...
]
The browser Geolocation API allows adopters to share their location. Request geolocation asynchronously and handle permission responses:
function getUserLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error("Geolocation not supported by this browser"));
}
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
resolve({ latitude, longitude });
},
(error) => {
reject(new Error(`Geolocation error: ${error.message}`));
}
);
});
}
Users should expect a browser permission prompt. If they deny access, the promise rejects and an error message displays. If they grant access, the coordinates resolve with latitude and longitude values.
Converting addresses to coordinates requires a geocoding service. Google Maps Platform and Mapbox both offer geocoding APIs. This example uses the Google Maps Geocoding API:
async function geocodeAddress(address, apiKey) {
const encodedAddress = encodeURIComponent(address);
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodedAddress}&key=${apiKey}`;
const response = await fetch(url);
const data = await response.json();
if (data.results.length === 0) {
throw new Error(`Couldn't geocode address: ${address}.`);
}
const { lat, lng } = data.results[0].geometry.location;
return { latitude: lat, longitude: lng };
}
Geocode all shelter addresses and attach coordinates to shelter objects:
async function enrichSheltersWithCoordinates(shelters, apiKey) {
const enrichedShelters = await Promise.all(
shelters.map(async (shelter) => {
try {
const coords = await geocodeAddress(shelter.address, apiKey);
return {
...shelter,
latitude: coords.latitude,
longitude: coords.longitude
};
} catch (error) {
console.error(`Failed to geocode ${shelter.name}:`, error);
// Return shelter without coordinates if geocoding fails
return shelter;
}
})
);
return enrichedShelters;
}
The Haversine formula calculates great-circle distances between two points on a sphere given their latitudes and longitudes. This implementation returns distance in miles:
function calculateDistance(lat1, lon1, lat2, lon2) {
const earthRadiusMiles = 3959;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) *
Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadiusMiles * c;
}
Add distance calculations to each shelter object:
function addDistancesToShelters(shelters, userLat, userLon) {
return shelters
.map((shelter) => ({
...shelter,
distance: calculateDistance(userLat, userLon, shelter.latitude, shelter.longitude)
}))
.sort((a, b) => a.distance - b.distance);
}
Filter shelter locations within a specified distance threshold. Allow potential adopters to choose their preferred search radius:
function filterSheltersByDistance(shelters, maxDistanceMiles) {
return shelters.filter((shelter) => shelter.distance <= maxDistanceMiles);
}
Combine the steps into a single function that orchestrates the entire flow:
async function searchNearestShelters(maxDistanceMiles = 10, mapsApiKey) {
try {
// Step 1: Get user location
const userLocation = await getUserLocation();
console.log("User location:", userLocation);
// Step 2: Fetch shelters from API
const shelterResponse = await fetch("{base_url}/shelters");
const shelters = await shelterResponse.json();
// Step 3: Geocode shelter addresses
const enrichedShelters = await enrichSheltersWithCoordinates(
shelters,
mapsApiKey
);
// Step 4: Calculate distances
const sheltersWithDistance = addDistancesToShelters(
enrichedShelters,
userLocation.latitude,
userLocation.longitude
);
// Step 5: Filter by radius
const nearbyShelters = filterSheltersByDistance(
sheltersWithDistance,
maxDistanceMiles
);
return nearbyShelters;
} catch (error) {
console.error("Error searching for nearby shelters:", error);
throw error;
}
}
Display filtered shelter profiles in a list with distance information:
async function displayNearbyShelters(maxDistanceMiles = 10, mapsApiKey) {
const shelterList = document.getElementById("shelter-list");
shelterList.innerHTML = "<p>Finding nearby shelters...</p>";
try {
const nearbyShelters = await searchNearestShelters(maxDistanceMiles, mapsApiKey);
if (nearbyShelters.length === 0) {
shelterList.innerHTML = "<p>No shelters found within your search radius.</p>";
return;
}
const html = nearbyShelters
.map(
(shelter) => `
<div class="shelter-card">
<h3>${shelter.name}</h3>
<p><strong>Address:</strong> ${shelter.address}</p>
<p><strong>Distance:</strong> ${shelter.distance.toFixed(1)} miles</p>
<p><strong>Phone:</strong> ${shelter.phone}</p>
<p><strong>Hours:</strong> ${shelter.hours}</p>
<a href="${shelter.website}" target="_blank">Visit Website</a>
</div>
`
)
.join("");
shelterList.innerHTML = html;
} catch (error) {
shelterList.innerHTML = `<p class="error">Error: ${error.message}</p>`;
}
}
HTML structure:
<div class="search-container">
<label for="distance-input">Search radius (miles):</label>
<input type="number" id="distance-input" value="10" min="1" max="50"/>
<button id="search-button">Find Nearby Shelters</button>
</div>
<div id="shelter-list"></div>
JavaScript event handler:
document.getElementById("search-button").addEventListener("click", () => {
const maxDistance = document.getElementById("distance-input").value;
const mapsApiKey = "GOOGLE_MAPS_API_KEY";
displayNearbyShelters(maxDistance, mapsApiKey);
});
navigator.geolocation before attempting to access location data.
For development, use mock coordinates to test the filtering logic.Promise.all() to geocode addresses in parallel.
Consider caching geocoded coordinates to avoid
re-geocoding the same addresses on future searches.status
to observe pet availability changes in real time.