Demo project created for my step-by-step article on custom maps with DeckGL, IconLayer, Next.js, and Geoapify.
Live demo: https://miami-food-map.vercel.app.
The goal of this project was to build an application that presents restaurants and food-related points of interest in Miami. For this purpose, I used data from the Geoapify Places API. The data returned by Geoapify is provided in GeoJSON format. In my tutorial, I demonstrate how to format this data, assign it to custom restaurant categories (pizza, burger, sandwich, seafood, asian, latin, and other), and visualize it on a map using Deck.gl technology.
The application includes:
- Data routing from Geoapify.
- Functions for transforming data and assigning it to categories.
- A React hook for creating superclusters, i.e. grouping data into clusters across different zoom levels.
- Data visualization using Deck.gl with IconLayer, TextLayer, and tooltips.
Clone the repository:
git clone https://github.com/Pyother/miami-food-map.git
cd miami-food-mapInstall dependencies:
npm installCreate .env file with variables:
GEOAPIFY_API_KEY=your_api_key_here
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_MAP_STYLE=https://maps.geoapify.com/v1/styles/dark-matter/style.json?apiKey=your_api_key_here
You can get your API key and browse available map styles from: https://www.geoapify.com. Replace your_api_key_here in both variables with your actual key.
Start the development server:
npm run dev
Then open: http://localhost:3000.
Unit tests are written with Vitest and cover two utility functions:
getLocationType— verifies that Geoapify category strings are correctly mapped to food types (burger,pizza,seafood, etc.) and that unknown or missing categories fall back to"food".mapClusterToLocation— verifies that individual place features and cluster features are correctly transformed intoLocationobjects, including id generation, position, type, and fallback behaviour.
Run tests with:
npm run testSince the app was built as a tutorial, it has some limitations:
- Static data — place data is fetched once at build time and baked into the static page. New or closed venues are not reflected until the app is rebuilt and redeployed.
- Hard-coded result cap — the Geoapify query is limited to 500 results (
SEARCH_LIMIT). If there are more than 500 matching places in the area, the excess is silently dropped. - Miami only — the search area is fixed to a single Geoapify place ID representing Miami. The app does not support other cities or dynamic area selection.
- Supercluster recreated on every render —
useSuperclusterrebuilds the entireSuperclusterindex on every render because it has no memoization. For 500 points this is acceptable, but it would become a performance issue with larger datasets. - Category fallback is lossy — places whose Geoapify categories do not match any entry in
FOOD_CATEGORIESare silently assigned the generic"food"icon instead of surfacing an unknown category. - No error handling for failed fetches — if the Geoapify API call fails at build time, the build errors out entirely rather than falling back to cached or empty data.
- Demo API key — the repository ships with a shared demo API key that Geoapify may restrict or revoke at any time (see comment in
route.tsandpage.tsx).