React15 min setupIntermediate

Build a ZIP Code Radius Search in React

Create a powerful location finder that searches for places within a radius of a ZIP code. Perfect for store locators, service area tools, and delivery zone calculators.

The Problem

Users need to find stores, service locations, or delivery options near their ZIP code. Calculating distances manually or using complex geolocation libraries is time-consuming and error-prone.

The Solution

Use the ZIP Code API's radius search endpoint to find all ZIP codes within a specified distance. Then match your store locations against the results to show only nearby options.

1Get Your API Key

Sign up on RapidAPI and get your API key for ZIP Code API:

Get Free API Key →

2Create Backend API Route (Next.js)

app/api/radius-search/route.ts

import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const zipcode = searchParams.get('zipcode');
  const radius = searchParams.get('radius') || '25'; // Default 25 miles
  
  if (!zipcode) {
    return NextResponse.json(
      { error: 'ZIP code is required' },
      { status: 400 }
    );
  }
  
  try {
    const response = await fetch(
      `https://zip-code-api8.p.rapidapi.com/radius?zipcode=${zipcode}&radius=${radius}`,
      {
        headers: {
          'X-RapidAPI-Key': process.env.RAPIDAPI_KEY!,
          'X-RapidAPI-Host': 'zip-code-api8.p.rapidapi.com'
        },
        next: { revalidate: 86400 } // Cache for 24 hours
      }
    );
    
    if (!response.ok) {
      throw new Error('Failed to fetch radius data');
    }
    
    const data = await response.json();
    
    return NextResponse.json(data);
    
  } catch (error) {
    console.error('Radius search error:', error);
    return NextResponse.json(
      { error: 'Failed to search radius' },
      { status: 500 }
    );
  }
}

Security Note: Never expose your RapidAPI key in the frontend. Always proxy requests through your backend to keep credentials secure.

3Build the Store Locator Component

components/StoreLocator.tsx

'use client';

import { useState } from 'react';

interface Store {
  id: string;
  name: string;
  zipcode: string;
  address: string;
  city: string;
  state: string;
}

// Sample store locations (replace with your actual data)
const STORES: Store[] = [
  { id: '1', name: 'Downtown Store', zipcode: '90210', address: '123 Main St', city: 'Beverly Hills', state: 'CA' },
  { id: '2', name: 'Westside Location', zipcode: '90211', address: '456 Oak Ave', city: 'Beverly Hills', state: 'CA' },
  { id: '3', name: 'Airport Plaza', zipcode: '90045', address: '789 LAX Blvd', city: 'Los Angeles', state: 'CA' },
  { id: '4', name: 'Beach Store', zipcode: '90401', address: '321 Beach Dr', city: 'Santa Monica', state: 'CA' },
];

export default function StoreLocator() {
  const [zipcode, setZipcode] = useState('');
  const [radius, setRadius] = useState('25');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [nearbyStores, setNearbyStores] = useState<Store[]>([]);
  const [searchPerformed, setSearchPerformed] = useState(false);
  
  const handleSearch = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    setSearchPerformed(true);
    
    try {
      // Fetch ZIP codes within radius
      const response = await fetch(
        `/api/radius-search?zipcode=${zipcode}&radius=${radius}`
      );
      
      if (!response.ok) {
        throw new Error('Failed to search');
      }
      
      const data = await response.json();
      
      // Extract ZIP codes from radius search results
      const nearbyZipCodes = new Set(
        data.results?.map((result: any) => result.zipcode) || []
      );
      
      // Filter stores that match nearby ZIP codes
      const filtered = STORES.filter(store => 
        nearbyZipCodes.has(store.zipcode)
      );
      
      setNearbyStores(filtered);
      
    } catch (err) {
      setError('Failed to search for stores. Please try again.');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
        <h2 className="text-2xl font-bold mb-6">Find Stores Near You</h2>
        
        {/* Search Form */}
        <form onSubmit={handleSearch} className="mb-8">
          <div className="grid md:grid-cols-3 gap-4">
            <div>
              <label className="block text-sm font-medium mb-2">
                ZIP Code
              </label>
              <input
                type="text"
                value={zipcode}
                onChange={(e) => setZipcode(e.target.value)}
                placeholder="90210"
                className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"
                required
                pattern="[0-9]{5}"
                maxLength={5}
              />
            </div>
            
            <div>
              <label className="block text-sm font-medium mb-2">
                Radius (miles)
              </label>
              <select
                value={radius}
                onChange={(e) => setRadius(e.target.value)}
                className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"
              >
                <option value="10">10 miles</option>
                <option value="25">25 miles</option>
                <option value="50">50 miles</option>
                <option value="100">100 miles</option>
              </select>
            </div>
            
            <div className="flex items-end">
              <button
                type="submit"
                disabled={loading}
                className="w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {loading ? 'Searching...' : 'Find Stores'}
              </button>
            </div>
          </div>
        </form>
        
        {/* Error Message */}
        {error && (
          <div className="mb-6 p-4 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded-lg text-red-700 dark:text-red-400">
            {error}
          </div>
        )}
        
        {/* Results */}
        {searchPerformed && !loading && (
          <div>
            <h3 className="text-xl font-semibold mb-4">
              {nearbyStores.length > 0 
                ? `Found ${nearbyStores.length} store${nearbyStores.length !== 1 ? 's' : ''} within ${radius} miles`
                : 'No stores found in this area'
              }
            </h3>
            
            {nearbyStores.length > 0 && (
              <div className="space-y-4">
                {nearbyStores.map(store => (
                  <div 
                    key={store.id}
                    className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 transition-colors"
                  >
                    <h4 className="font-semibold text-lg">{store.name}</h4>
                    <p className="text-gray-600 dark:text-gray-400 mt-1">
                      {store.address}
                    </p>
                    <p className="text-gray-600 dark:text-gray-400">
                      {store.city}, {store.state} {store.zipcode}
                    </p>
                    <button className="mt-3 text-blue-600 hover:text-blue-700 font-medium">
                      Get Directions →
                    </button>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

4Use the Component in Your App

app/store-locator/page.tsx

import StoreLocator from '@/components/StoreLocator';

export default function StoreLocatorPage() {
  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12">
      <StoreLocator />
    </div>
  );
}

What the API Returns

Example Response

Radius Search for 90210 within 25 miles:

{
  "success": true,
  "center": {
    "zipcode": "90210",
    "city": "Beverly Hills",
    "state": "CA",
    "latitude": 34.0901,
    "longitude": -118.4065
  },
  "results": [
    {
      "zipcode": "90211",
      "city": "Beverly Hills",
      "state": "CA",
      "distance": 1.2,
      "latitude": 34.0736,
      "longitude": -118.3987
    },
    {
      "zipcode": "90212",
      "city": "Beverly Hills",
      "state": "CA",
      "distance": 2.5,
      "latitude": 34.0669,
      "longitude": -118.4058
    },
    // ... more ZIP codes within radius
  ],
  "total_results": 47
}

Advanced Enhancements

Add Distance Calculation

Show exact distance to each store using the API's distance data:

// Enhance store matching with distance info
const storesWithDistance = STORES.map(store => {
  const radiusResult = data.results?.find(
    (r: any) => r.zipcode === store.zipcode
  );
  
  return {
    ...store,
    distance: radiusResult?.distance || null
  };
}).filter(store => store.distance !== null)
  .sort((a, b) => a.distance! - b.distance!); // Sort by closest

// Display distance in results
<p className="text-sm text-gray-500">
  {store.distance?.toFixed(1)} miles away
</p>

Add Map Visualization

Integrate with Google Maps or Mapbox to show visual results:

import { GoogleMap, Marker } from '@react-google-maps/api';

// Add markers for each store
{nearbyStores.map(store => (
  <Marker
    key={store.id}
    position={{ 
      lat: store.latitude, 
      lng: store.longitude 
    }}
    title={store.name}
  />
))}

Cache Results with React Query

Reduce API calls by caching search results:

import { useQuery } from '@tanstack/react-query';

const { data, isLoading, error } = useQuery({
  queryKey: ['radius-search', zipcode, radius],
  queryFn: async () => {
    const res = await fetch(
      `/api/radius-search?zipcode=${zipcode}&radius=${radius}`
    );
    return res.json();
  },
  staleTime: 1000 * 60 * 60, // Cache for 1 hour
  enabled: zipcode.length === 5
});

Auto-detect User Location

Use browser geolocation to pre-fill the user's ZIP code:

const detectLocation = () => {
  if ('geolocation' in navigator) {
    navigator.geolocation.getCurrentPosition(async (position) => {
      // Use reverse geocoding API to get ZIP from coordinates
      const { latitude, longitude } = position.coords;
      
      const res = await fetch(
        `/api/reverse-geocode?lat=${latitude}&lng=${longitude}`
      );
      const data = await res.json();
      
      setZipcode(data.zipcode);
    });
  }
};

Next Steps

  • 1.Get your free API key from RapidAPI
  • 2.Add your API key to .env.local as RAPIDAPI_KEY
  • 3.Copy the API route and component code above
  • 4.Replace the sample STORES array with your actual store data
  • 5.Test with different ZIP codes and radius values

Ready to Build Your Store Locator?

Get your free API key and start building location-aware features today.

Get Free API Key →