How to Add Google Maps with Dynamic Markers in Django

Blog / Django · February 24, 2016 · Updated June 10, 2026 · 8 min read
How to Add Google Maps with Dynamic Markers in Django

To show a Google Map with markers pulled from your Django models, you (1) store latitude and longitude on each record, (2) hand those coordinates to the page as JSON with Django's json_script filter (or a small JSON endpoint), and (3) load the Maps JavaScript API with the dynamic library loader and loop over the data to drop one AdvancedMarkerElement per record, each with an InfoWindow, then call map.fitBounds() so every pin is visible.

Two things have changed since this article was first written. Google Maps now requires an API key tied to a billing-enabled Google Cloud project — the old keyless embed no longer works. And the classic google.maps.Marker is deprecated (since 2024); new code should use AdvancedMarkerElement, which needs a Map ID and the marker library.

Key takeaways

  • An API key is mandatory. Create a Google Cloud project, enable the Maps JavaScript API, attach a billing account (it includes a monthly free usage allowance), and restrict the key to your domains.
  • Use AdvancedMarkerElement, not Marker. The classic marker is deprecated; the advanced marker requires a mapId and the marker library.
  • Keep coordinates server-side. Render lat/lng from your queryset with json_script or serve them from a JSON endpoint — never build the JSON by hand in a string.
  • Geocode on the server. Convert addresses to coordinates once with the Geocoding API and store them, rather than geocoding on every page load.
  • Building data-rich, map-driven pages is everyday Django development and web development work.

What changed with Google Maps, and why your old code breaks

If you copied a Google Maps snippet a few years ago, two parts of it are now obsolete: the keyless script tag and new google.maps.Marker(). Here is how the old and new approaches compare.

Concern Old approach (legacy) Modern approach (2024+)
Authentication Keyless or unrestricted key API key on a billing-enabled project, domain-restricted
Script loading <script src="...&callback=initMap"> Dynamic library loader + importLibrary()
Marker class google.maps.Marker (deprecated) google.maps.marker.AdvancedMarkerElement
Required extras none mapId + the marker library
Customising the pin raster icon image any HTML/DOM element or PinElement

AdvancedMarkerElement is not just a rename. Because each marker is a real DOM element, you can style pins with HTML/CSS, drop in your own logos, and attach normal event listeners — something the old raster icon could never do.

How do you pass Django model coordinates to the map?

Start with a model that stores coordinates. If you only have addresses, geocode them once (see below) and save the result so the page never has to geocode at render time.

# locations/models.py
from django.db import models


class Location(models.Model):
    name = models.CharField(max_length=200)
    address = models.CharField(max_length=300, blank=True)
    latitude = models.DecimalField(max_digits=9, decimal_places=6)
    longitude = models.DecimalField(max_digits=9, decimal_places=6)

    def __str__(self):
        return self.name

    @property
    def as_marker(self):
        """Compact dict the template and JS can consume directly."""
        return {
            "name": self.name,
            "address": self.address,
            "lat": float(self.latitude),
            "lng": float(self.longitude),
        }

There are two clean ways to get that queryset into the browser:

  1. json_script (simplest) — Django renders the data inside a safe <script type="application/json"> tag that your JS reads with JSON.parse. No extra request, and no XSS risk from hand-built strings.
  2. A JSON endpoint — return the markers from a view (plain JsonResponse or Django REST Framework) and fetch() them. Better when the map updates without a full page reload or the dataset is large.

The view below prepares the marker list and also exposes a JSON endpoint, so you can use either pattern. The API key and Map ID come from settings, never a hardcoded literal.

# locations/views.py
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import render

from .models import Location


def map_page(request):
    markers = [loc.as_marker for loc in Location.objects.all()]
    context = {
        "markers": markers,
        "maps_api_key": settings.GOOGLE_MAPS_BROWSER_KEY,
        "maps_map_id": settings.GOOGLE_MAPS_MAP_ID,
    }
    return render(request, "locations/map.html", context)


def markers_json(request):
    """Optional endpoint for fetch()-driven maps."""
    markers = [loc.as_marker for loc in Location.objects.all()]
    return JsonResponse({"markers": markers})

How do you load the map and drop the markers?

The template needs just three things: a <div> for the map, the queryset serialised with json_script, and a small config element carrying the (public) browser key and Map ID. A module script then reads that data and loops over it.

{# locations/templates/locations/map.html #}
{% load static %}

<div id="map" style="height: 500px;"></div>

{# Safely hand the queryset to JS — Django escapes it for you #}
{{ markers|json_script:"markers-data" }}

{# Public browser key + Map ID, injected from settings #}
<div id="map-config"
     data-key="{{ maps_api_key }}"
     data-map-id="{{ maps_map_id }}"></div>

<script type="module" src="{% static 'locations/map.js' %}"></script>
// locations/static/locations/map.js
// Server-provided config + data — no secrets are hardcoded in this file.
const config = document.getElementById('map-config').dataset;
const points = JSON.parse(document.getElementById('markers-data').textContent);

// Official Google Maps "dynamic library import" bootstrap loader. It replaces the
// legacy `<script src="...&callback=initMap">` tag and lets you await libraries.
(g => { var h, a, k, p = "The Google Maps JavaScript API", c = "google", l = "importLibrary", q = "__ib__", m = document, b = window; b = b[c] || (b[c] = {}); var d = b.maps || (b.maps = {}), r = new Set, e = new URLSearchParams, u = () => h || (h = new Promise(async (f, n) => { await (a = m.createElement("script")); e.set("libraries", [...r] + ""); for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]); e.set("callback", c + ".maps." + q); a.src = `https://maps.${c}apis.com/maps/api/js?` + e; d[q] = f; a.onerror = () => h = n(Error(p + " could not load.")); a.nonce = m.querySelector("script[nonce]")?.nonce || ""; m.head.append(a) })); d[l] ? console.warn(p + " only loads once. Ignoring:", g) : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n)) })({ key: config.key, v: "weekly" });

async function initMap() {
  // Importing 'marker' tells the loader to add &libraries=marker for you.
  const { Map, InfoWindow } = await google.maps.importLibrary('maps');
  const { AdvancedMarkerElement } = await google.maps.importLibrary('marker');

  const map = new Map(document.getElementById('map'), {
    mapId: config.mapId,            // mapId is required by AdvancedMarkerElement
    center: { lat: 17.385, lng: 78.4867 },
    zoom: 5,
  });

  const info = new InfoWindow();
  const bounds = new google.maps.LatLngBounds();

  for (const point of points) {
    const position = { lat: point.lat, lng: point.lng };
    const marker = new AdvancedMarkerElement({ map, position, title: point.name });

    marker.addListener('gmp-click', () => {
      info.setContent(`<strong>${point.name}</strong><br>${point.address ?? ''}`);
      info.open({ map, anchor: marker });
    });

    bounds.extend(position);
  }

  // Auto-frame every marker instead of guessing a center and zoom level.
  if (points.length) map.fitBounds(bounds);
}

initMap();

How do you turn an address into coordinates?

AdvancedMarkerElement needs latitude and longitude, not a street address. If your users only enter addresses, geocode them on the server with the Geocoding API and store the result — geocoding the same address on every page load wastes quota and slows the page.

# locations/services.py
import requests
from django.conf import settings


def geocode(address):
    """Return (lat, lng) for an address via the Geocoding API, or None."""
    resp = requests.get(
        "https://maps.googleapis.com/maps/api/geocode/json",
        params={"address": address, "key": settings.GOOGLE_MAPS_SERVER_KEY},
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()
    if data.get("status") != "OK":
        return None
    location = data["results"][0]["geometry"]["location"]
    return location["lat"], location["lng"]

How do you keep your API key safe?

A Maps browser key is visible in page source — that is expected — so security comes from restriction, not secrecy. Lock the browser key to your domains (HTTP referrers) and to only the APIs it needs. Use a separate server key for the Geocoding call and restrict it by IP. Never hardcode either value; load them from environment variables through Django settings.

# settings.py — read keys from the environment, never commit them
import os

GOOGLE_MAPS_BROWSER_KEY = os.environ["GOOGLE_MAPS_BROWSER_KEY"]  # HTTP-referrer restricted
GOOGLE_MAPS_SERVER_KEY = os.environ["GOOGLE_MAPS_SERVER_KEY"]    # IP restricted
GOOGLE_MAPS_MAP_ID = os.environ["GOOGLE_MAPS_MAP_ID"]

Do you have to use Google Maps?

No. Google Maps is the richest option, but it requires a billing account. If you want to avoid that entirely, open-source map stacks render markers just as well from the same Django JSON.

Option API key / billing Best for
Maps JavaScript API Key + billing account required Rich Google data, Street View, Places
Static Maps / iframe embed Key required, no JS A single, non-interactive pin
Leaflet + OpenStreetMap No key, no billing Interactive maps with zero platform fees
Mapbox GL JS Free tier, key required Custom-styled, high-volume vector maps

Whichever you choose, the Django side is identical — you still expose the queryset as JSON. Calling third-party services like these from Django follows the same pattern we cover in integrating an external API in Django.

Frequently Asked Questions

Do I need an API key and billing to use Google Maps in Django?

Yes. Google now requires an API key tied to a billing-enabled Google Cloud project; the old keyless embed no longer works. Billing accounts include a monthly free usage allowance, and you can cap spend with quotas. To avoid billing entirely, use Leaflet with OpenStreetMap instead.

Why is google.maps.Marker not working anymore?

The classic google.maps.Marker was deprecated in 2024. New code should use google.maps.marker.AdvancedMarkerElement, which loads from the marker library and requires a Map ID on the map. Existing Marker code still runs for now, but you should migrate it.

How do I pass Django queryset data to JavaScript safely?

Use Django's json_script template filter. It renders your data inside a <script type="application/json"> tag that JavaScript reads with JSON.parse, escaping the data correctly and avoiding the XSS risk of building JSON strings by hand. For maps that refresh without a reload, serve the data from a JSON endpoint and fetch() it.

How do I fit the map so all markers are visible?

Create a google.maps.LatLngBounds object, call bounds.extend() with each marker's position inside your loop, then call map.fitBounds(bounds) after the loop. The map automatically pans and zooms so every marker is in view, so you never have to guess a center or zoom level.

Should I geocode addresses in the browser or on the server?

Geocode on the server with the Geocoding API and store the resulting coordinates on your model. Geocoding the same address on every page load wastes quota and slows rendering. Use a separate, IP-restricted server key for these calls and keep it out of the page source.

How do I add an InfoWindow to an AdvancedMarkerElement?

Create one shared InfoWindow, then add a gmp-click listener to each AdvancedMarkerElement. In the handler, call info.setContent() with your HTML and info.open({ map, anchor: marker }). Reusing a single InfoWindow keeps only one popup open at a time.

Mapping store locations, fleets, or user-submitted places onto an interactive map is a common request once a Django app gathers address data. The recipe above — coordinates on the model, JSON to the page, and AdvancedMarkerElement in the browser — scales from three pins to thousands with clustering, and works the same whether you stay on Google Maps or move to an open-source stack.

Share this article