In this tutorial, you’ll learn how to use the Geoapify Places API with Leaflet to display nearby places — such as parks, museums, and cinemas — on an interactive map.
Try the live example: ➡️ View on CodePen
In this part, we’ll focus on the basics:
- Querying the Places API by category
- Parsing and displaying GeoJSON results
- Styling map markers with the Geoapify Marker Icon API and showing popups in Leaflet
🧭 Table of Contents
- Step 1 – Set up the Leaflet Map
- [Step 2 – Query Places by Category Using the Geoapify Places API](#step-2-query-places-by-category-using-the-geoapify-p…
In this tutorial, you’ll learn how to use the Geoapify Places API with Leaflet to display nearby places — such as parks, museums, and cinemas — on an interactive map.
Try the live example: ➡️ View on CodePen
In this part, we’ll focus on the basics:
- Querying the Places API by category
- Parsing and displaying GeoJSON results
- Styling map markers with the Geoapify Marker Icon API and showing popups in Leaflet
🧭 Table of Contents
- Step 1 – Set up the Leaflet Map
- Step 2 – Query Places by Category Using the Geoapify Places API
- Step 3 – Display Places as Custom Markers (Marker Icon API)
- Summary
Step 1 – Set up the Leaflet Map
Let’s start by setting up a basic Leaflet map with Geoapify basemap tiles.
Tip: You don’t need to build it from scratch — use this ready-to-edit CodePen template: ➡️ Open Leaflet Map Template It will create a new Codepen with a Leaflet map. Please replace the template Geoapify API key with your API key. Register and get your Free API key here.
Here are main steps to create a map:
const yourAPIKey = "YOUR_GEOAPIFY_API_KEY";
// Create the map and center it on London
const map = L.map("map").setView([51.5074, -0.1278], 14);
// Detect high-resolution displays for crisper tiles
const isRetina = window.devicePixelRatio > 1;
// Use Geoapify tiles — regular or @2x for retina
const baseUrl = `https://maps.geoapify.com/v1/tile/klokantech-basic/{z}/{x}/{y}.png?apiKey=${yourAPIKey}`;
const retinaUrl = `https://maps.geoapify.com/v1/tile/klokantech-basic/{z}/{x}/{y}@2x.png?apiKey=${yourAPIKey}`;
// Add the tile layer
L.tileLayer(isRetina ? retinaUrl : baseUrl, {
attribution:
'Powered by <a href="https://www.geoapify.com/" target="_blank">Geoapify</a> | <a href="https://openmaptiles.org/" target="_blank">© OpenMapTiles</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap</a> contributors',
maxZoom: 20,
id: "osm-bright",
apiKey: yourAPIKey
}).addTo(map);
This creates a Leaflet map centered on London using the Geoapify “klokantech-basic” tile style.
The isRetina check ensures that users on high-DPI displays see sharp, high-resolution tiles.
Next, we’ll connect the Geoapify Places API to query and show real places on the map.
Step 2 – Query Places by Category Using the Geoapify Places API
Now that the map is ready, let’s make it interactive by automatically loading places whenever the user pans or zooms.
We’ll connect to the Geoapify Places API and fetch real locations — such as museums, parks, or cinemas — based on the current map view.
1. Listen to map movement events
We want to detect when the user moves or zooms the map, then trigger a new API request.
Leaflet makes this easy with moveend and zoomend events:
map.on("moveend", loadPlaces);
map.on("zoomend", loadPlaces);
Whenever the map position changes, the loadPlaces() function will run and update the visible results.
2. Load places from the Geoapify Places API
Inside loadPlaces(), we:
- Get the map’s current bounds.
- Define which categories we want to search (for example, museums, cinemas, or parks).
- Build and send the API request.
async function loadPlaces() {
const bounds = map.getBounds();
const rect = `${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`;
const categories = "entertainment.museum,entertainment.cinema,leisure.park";
const url = `https://api.geoapify.com/v2/places?categories=${categories}&filter=rect:${rect}&limit=100&apiKey=${yourAPIKey}`;
const response = await fetch(url);
const data = await response.json();
console.log("Places loaded:", data);
}
Tip: The key part is
filter=rect:${rect}— it limits the search to the current map bounds, so the API only returns places visible on the map. This makes the search faster, more relevant, and efficient.You can explore all available categories in the Places API docs.
This is the simplest version of a map-based search — it calls the API every time the user stops moving the map and retrieves up to 100 places in the visible area.
⚙️ In real-world apps
For better performance and smoother user experience, you’ll want to:
- Add a short delay (debounce) between map movement and the API request.
- Split large map areas into smaller fragments to avoid long or heavy queries.
- Limit requests by zoom level (e.g., only start searching when
zoom >= 13). - Prevent duplicate results if multiple requests overlap.
- Rate-limit API calls to stay within usage limits.
All of these optimizations are implemented in the full CodePen example: ➡️ View the complete version on CodePen
Step 3 – Display places as custom markers (Marker Icon API)
We’ll parse the GeoJSON from the Places API and render each Point as a Leaflet marker.
To make markers visually meaningful, we’ll generate icon URLs on the fly with the Geoapify Marker Icon API.
1) Create a marker icon helper
function markerIconUrl(iconName = "map-marker", color = "#37a961") {
const icon = encodeURIComponent(iconName);
const tint = encodeURIComponent(color);
return `https://api.geoapify.com/v2/icon/?type=awesome&icon=${icon}&color=${tint}&size=50&contentSize=20&scaleFactor=2&apiKey=${yourAPIKey}`;
}
This will create a custom marker icon sized about 38×55 px — the visible icon itself is 38×50 px, and the subtle drop shadow adds roughly 5 px for depth.
The scaleFactor=2 makes icons sharp on retina screens by rendering them at double resolution.
2) Render features as markers with popups
Add this after loadPlaces() fetches the data:
function renderPlaces(geojson) {
if (!geojson || !Array.isArray(geojson.features)) return;
// Optional: group for easy clear/redraw on next search
if (!window.resultsLayer) {
window.resultsLayer = L.layerGroup().addTo(map);
} else {
window.resultsLayer.clearLayers();
}
geojson.features.forEach((f) => {
const { geometry, properties } = f;
if (!geometry || geometry.type !== "Point") return;
const [lng, lat] = geometry.coordinates;
// Pick an icon based on categories (fallback to a generic one)
const iconName = "map-marker"; // you can map categories -> icon names if you like
const icon = L.icon({
iconUrl: markerIconUrl(iconName, "#22c55e"),
iconSize: [38, 55], // Icon image size in pixels (width x height)
iconAnchor: [18, 50], // The point of the icon (relative to its top-left corner) that will correspond to the marker’s geographical location
popupAnchor: [0, -50] // Offset of the popup relative to the icon anchor. Moves the popup slightly above the icon tip
});
const name = properties?.name || properties?.address_line1 || "Unnamed place";
const address = properties?.address_line2 || properties?.formatted || "";
L.marker([lat, lng], { icon })
.bindPopup(`<strong>${escapeHtml(name)}</strong><br>${escapeHtml(address)}`)
.addTo(window.resultsLayer);
});
}
// Small helper to avoid HTML injection in popups
function escapeHtml(s = "") {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
Call renderPlaces(data) at the end of loadPlaces():
const data = await response.json();
renderPlaces(data);
What you get
- Custom icons generated by the Marker Icon API (fast, no image hosting needed).
- Informative popups with place names and addresses.
- A clean layer group so the map updates neatly on each search.
Tip: For even better UX, map specific categories (e.g.,
entertainment.museum,leisure.park) to different icon names or colors so users can visually distinguish them at a glance.
Summary
In this tutorial, we built an interactive Leaflet map powered by the Geoapify Places API. You learned how to:
- Set up a map with Geoapify basemap tiles
- Query the Places API by category and map bounds
- Parse GeoJSON results
- Display results as custom markers using the Geoapify Marker Icon API
The result is a map that dynamically shows real places — like museums, cinemas, or parks — with meaningful icons and popups.
For smoother performance and a more responsive UX, check out the advanced version of this project: ➡️ Full CodePen example
In the next tutorial, we’ll dive deeper into performance optimizations — including chunked API requests, rate limiting, and progressive loading for large map areas.