Draw GeoJSON points as a layer with MapLibre GL (Mapbox GL)

Geoapify returns results in GeoJSON format which is natively supported by most of the client-side map libraries. Our Geocoding API and Places API return a GeoJSON.FeatureCollection with Features with geometry type Point.

In this code sample, we would like to show how GeoJSON points can be added to a map and handled as a layer. The full version of the code sample you can find on JSFiddle.

1. Create a map

You require a Geoapify API key to make API calls and display a map. Register and get an API key on MyProjects Geoapify.com.

We use MapLibre GL as a client-side library which is an open-source fork of Mapbox GL library before their switch to a non-OSS license.

You can include the library directly to your HTML-page or install it with npm:

<link rel="stylesheet" type="text/css" href="https://cdn.skypack.dev/maplibre-gl/dist/maplibre-gl.css">

Add a map container to your HTML-template and initilize the map:

<div id="my-map"></div>

body {
    margin: 0;
    padding: 0;

#my-map {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 100%;
import { Map, NavigationControl, Popup } from 'https://cdn.skypack.dev/maplibre-gl';

var bounds = {
  // Paris
  lat1: 48.88002146841028,
  lon1: 2.3410839716279455,
  lat2: 48.86395628860821,
  lon2: 2.368348737185606

var map = new Map({
  center: [(bounds.lon1 + bounds.lon2) / 2, (bounds.lat1 + bounds.lat2) / 2],
  zoom: 15,
  container: 'my-map',
  style: `https://maps.geoapify.com/v1/styles/klokantech-basic/style.json?apiKey=${YOUR_API_KEY}`,
map.addControl(new NavigationControl());

2. Query places for the given area

We use Geoapify Places API to get places of type 'cafe'. You can query the data with the following code:

var type = "cafe"

// getting cafes for the given boundary (number of results limited by 100)
var placesUrl = `https://api.geoapify.com/v1/places?lat1=${bounds.lat1}&lon1=${bounds.lon1}&lat2=${bounds.lat2}&lon2=${bounds.lon2}&type=${type}&limit=100&apiKey=${YOUR_API_KEY}`;

fetch(placesUrl).then(response => response.json()).then(places => {

3. Add results as a layer to the map

MapLibre GL lets to load an image and set it as an icon for the layer. We use a Geoapify Marker Icon API to generate an icon:

// getting an icon from Geoapify Icons API
map.loadImage(`https://api.geoapify.com/v1/icon/?icon=coffee&color=%23ff9999&size=large&type=awesome&apiKey=${YOUR_API_KEY}`, function(error, image) {
  if (error) throw error;
  map.addImage('rosa-pin', image); //38x55px, shadow adds 5px

Add the places to a map as a layer of type 'symbol':

fetch(placesUrl).then(response => response.json()).then(places => {
  showGeoJSONPoints(places, type);

function showGeoJSONPoints(geojson, id) {
  var layerId = `${id}-layer`;

  if (map.getSource(id)) {
    // romove first the old one

  map.addSource(id, {
    'type': 'geojson',
    'data': geojson

    'id': layerId,
    'type': 'symbol',
    'source': id,
    'layout': {
      'icon-image': 'rosa-pin',
      'icon-anchor': 'bottom',
      'icon-offset': [0, 5],
      'icon-allow-overlap': true

Note, the icons contain shadow, so we need to specify an icon offset to correct the icon position. Use Marker Icon API Playground to get the correct bottom offset.

Get a notification and show a popup when a user clicks on a place

The big advantage of the layer is that you add only 1 click event handler for the whole layer. However, you need to take care of some details:

  • Normalize coordinates;
  • Setup cursor value:
function showGeoJSONPoints(geojson, id) {

  map.on('click', layerId, function(e) {
    var coordinates = e.features[0].geometry.coordinates.slice();
    var name = e.features[0].properties.name;

    // Ensure that if the map is zoomed out such that multiple
    // copies of the feature are visible, the popup appears
    // over the copy being pointed to.
    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;

    new Popup({
        anchor: 'bottom',
        offset: [0, -50]

  // Change the cursor to a pointer when the mouse is over the places layer.
  map.on('mouseenter', layerId, function() {
    map.getCanvas().style.cursor = 'pointer';

  // Change it back to a pointer when it leaves.
  map.on('mouseleave', layerId, function() {
    map.getCanvas().style.cursor = '';


Note, the position of the popup also needs to be adjusted according to icon size.