Claude Code Google Maps Integration: Practical Next.js Guide
Use Claude Code to add Google Maps safely with Advanced Markers, geocoding, store locators, Mapbox, and checks.
Design the Map Before Rendering It
Claude Code can add a map widget quickly. The production work is harder: API key restrictions, billing alerts, address search, mobile layout, privacy, server-side geocoding, and the difference between Google Maps and Mapbox.
The mistake Masa hit in an early store-locator prototype was not the marker itself. It was address search. Geocoding means converting an address into latitude and longitude. It is useful, but addresses are ambiguous, quota-backed, and tied to provider policies. If you mix browser keys and server keys, the app may work in development and still be insecure in production.
This guide treats Claude Code as an implementation partner, not as a copy-paste snippet generator. The examples use Next.js App Router, React, Google Maps JavaScript API, Advanced Markers, Geocoding API, and a small Mapbox GL JS alternative. Pair this with Claude Code security audits, performance optimization, and data visualization when the map becomes part of a real product.
Give Claude Code an Implementation Brief
Start with operating constraints, not with the library name. A map feature affects UX, cost, and legal obligations, so the request should be explicit.
Implement a store locator in Next.js App Router.
Requirements:
- Use the Google Maps JavaScript API
- Use AdvancedMarkerElement, not the legacy Marker class
- Read the browser key from NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
- Assume HTTP referrer restrictions and API restrictions are configured
- Call the Geocoding API from a server route with GOOGLE_MAPS_SERVER_KEY
- Keep the server key out of the browser bundle
- Sync address search, store list, marker clicks, and selected state
- Never read window or google during server-side rendering
- Include loading, error, empty, permission-denied, and mobile states
- Return a final checklist for API restrictions, billing alerts, and policy review
The implementation is easier to review when you split responsibilities.
flowchart LR
User["User"]
Page["Store locator page"]
Map["Google Maps JS API"]
Route["/api/geocode"]
Google["Geocoding API"]
Store["Store data"]
Alerts["Billing alerts and logs"]
User --> Page
Page --> Map
Page --> Store
Page --> Route
Route --> Google
Route --> Alerts
Use plain language in the brief. Geocoding is “turning an address into coordinates.” Reverse geocoding is “turning coordinates into a likely address.” A map ID is “the Google Cloud identifier used for styled maps and Advanced Markers.” That wording helps product owners review the plan without guessing what the technical terms mean.
Load Google Maps Safely in Next.js
Install the loader and type definitions first. The type package is not just polish; it helps catch outdated examples when Claude Code uses an API that no longer matches the current Maps JavaScript API.
npm i @googlemaps/js-api-loader
npm i -D @types/google.maps
Google’s current Advanced Marker guidance requires a map ID. The DEMO_MAP_ID works for quick local checks, but production should use a map ID created in Google Cloud Console. The browser API key is visible by design for Maps JavaScript API, so the practical security control is to restrict it. Follow the official Google Maps Platform security guidance: use HTTP referrer restrictions and restrict the key to only the APIs it needs.
// src/lib/google-maps-loader.ts
import { Loader } from "@googlemaps/js-api-loader";
let googleMapsPromise: Promise<typeof google> | null = null;
export function loadGoogleMaps() {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new Error("NEXT_PUBLIC_GOOGLE_MAPS_API_KEY is missing");
}
if (!googleMapsPromise) {
const loader = new Loader({
apiKey,
version: "weekly",
libraries: ["marker", "places"],
});
googleMapsPromise = loader.load();
}
return googleMapsPromise;
}
Now create the map component. The important details are SSR safety, marker cleanup, and avoiding user-provided HTML inside an InfoWindow.
// src/components/GoogleBusinessMap.tsx
"use client";
import { useEffect, useRef } from "react";
import { loadGoogleMaps } from "@/lib/google-maps-loader";
export type MapPoint = {
id: string;
title: string;
lat: number;
lng: number;
category?: "store" | "warehouse" | "property";
};
type Props = {
points: MapPoint[];
center: google.maps.LatLngLiteral;
zoom?: number;
onSelect?: (point: MapPoint) => void;
};
export function GoogleBusinessMap({ points, center, zoom = 13, onSelect }: Props) {
const mapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let cancelled = false;
let markers: google.maps.marker.AdvancedMarkerElement[] = [];
async function renderMap() {
await loadGoogleMaps();
if (!mapRef.current || cancelled) return;
const { Map } = (await google.maps.importLibrary("maps")) as google.maps.MapsLibrary;
const { AdvancedMarkerElement, PinElement } = (await google.maps.importLibrary(
"marker",
)) as google.maps.MarkerLibrary;
const map = new Map(mapRef.current, {
center,
zoom,
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID ?? "DEMO_MAP_ID",
fullscreenControl: false,
gestureHandling: "cooperative",
});
markers = points.map((point, index) => {
const pin = new PinElement({
glyph: String(index + 1),
background: point.category === "warehouse" ? "#0f766e" : "#2563eb",
borderColor: "#ffffff",
glyphColor: "#ffffff",
});
const marker = new AdvancedMarkerElement({
map,
position: { lat: point.lat, lng: point.lng },
title: point.title,
content: pin.element,
});
marker.addListener("click", () => onSelect?.(point));
return marker;
});
}
renderMap().catch((error) => {
console.error("Failed to render Google Map", error);
});
return () => {
cancelled = true;
markers.forEach((marker) => {
marker.map = null;
});
};
}, [center.lat, center.lng, points, zoom, onSelect]);
return <div ref={mapRef} className="h-[420px] w-full rounded-lg border" />;
}
This gives you a reliable map surface. The next production boundary is address search.
Keep Geocoding on the Server
Use separate keys for browser maps and server geocoding. The browser key should be restricted by referrer. The server key should be restricted according to its execution environment, often by IP address for server-side web service calls. Check the official Geocoding request and response docs for status values and response structure.
// src/app/api/geocode/route.ts
import { NextResponse } from "next/server";
type GeocodeResponse = {
status: string;
error_message?: string;
results: Array<{
formatted_address: string;
place_id: string;
geometry: {
location: { lat: number; lng: number };
};
}>;
};
const endpoint = "https://maps.googleapis.com/maps/api/geocode/json";
export async function GET(request: Request) {
const key = process.env.GOOGLE_MAPS_SERVER_KEY;
const { searchParams } = new URL(request.url);
const address = searchParams.get("address")?.trim();
if (!key) {
return NextResponse.json({ error: "Server key is missing" }, { status: 500 });
}
if (!address || address.length > 180) {
return NextResponse.json({ error: "Address is required" }, { status: 400 });
}
const params = new URLSearchParams({
address,
key,
language: "en",
region: "us",
});
const response = await fetch(`${endpoint}?${params}`, { cache: "no-store" });
const data = (await response.json()) as GeocodeResponse;
const first = data.results[0];
if (!response.ok || data.status !== "OK" || !first) {
return NextResponse.json(
{ error: data.error_message ?? data.status },
{ status: data.status === "ZERO_RESULTS" ? 404 : 502 },
);
}
return NextResponse.json({
formattedAddress: first.formatted_address,
placeId: first.place_id,
location: first.geometry.location,
});
}
Do not add long-term caching just because quota is expensive. Geocoding storage and display rules depend on the Google Maps Platform terms and product policies. For a store locator, keep your official store coordinates in your own database, and use geocoding for user-entered searches or admin workflows where the policy is clear.
Build the Store Locator UI
The UI links address search, the list, the map center, and marker selection. This example is intentionally small enough to paste into a Next.js project and then adapt to your design system.
// src/components/StoreLocator.tsx
"use client";
import { useMemo, useState } from "react";
import { GoogleBusinessMap, type MapPoint } from "@/components/GoogleBusinessMap";
type Store = {
id: string;
name: string;
address: string;
hours: string;
phone: string;
position: google.maps.LatLngLiteral;
};
type GeocodeResult = {
formattedAddress: string;
placeId: string;
location: google.maps.LatLngLiteral;
};
const defaultCenter = { lat: 40.7128, lng: -74.006 };
export function StoreLocator({ stores }: { stores: Store[] }) {
const [query, setQuery] = useState("");
const [center, setCenter] = useState(stores[0]?.position ?? defaultCenter);
const [selectedId, setSelectedId] = useState(stores[0]?.id ?? "");
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const points = useMemo<MapPoint[]>(
() =>
stores.map((store) => ({
id: store.id,
title: store.name,
lat: store.position.lat,
lng: store.position.lng,
category: "store",
})),
[stores],
);
async function searchAddress() {
if (!query.trim()) return;
setStatus("loading");
const response = await fetch(`/api/geocode?address=${encodeURIComponent(query)}`);
if (!response.ok) {
setStatus("error");
return;
}
const result = (await response.json()) as GeocodeResult;
setCenter(result.location);
setStatus("idle");
}
return (
<section className="grid gap-4 lg:grid-cols-[320px_1fr]">
<aside className="space-y-3">
<div className="flex gap-2">
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && searchAddress()}
placeholder="Search by address or city"
className="min-w-0 flex-1 rounded-md border px-3 py-2"
/>
<button onClick={searchAddress} className="rounded-md bg-blue-600 px-4 py-2 text-white">
Search
</button>
</div>
{status === "error" && <p className="text-sm text-red-600">No matching address found.</p>}
<div className="max-h-[420px] space-y-2 overflow-auto">
{stores.map((store) => (
<button
key={store.id}
onClick={() => {
setSelectedId(store.id);
setCenter(store.position);
}}
className={`w-full rounded-md border p-3 text-left ${
selectedId === store.id ? "border-blue-500 bg-blue-50" : "bg-white"
}`}
>
<span className="block font-medium">{store.name}</span>
<span className="block text-sm text-gray-600">{store.address}</span>
<span className="block text-sm text-gray-500">{store.hours}</span>
</button>
))}
</div>
</aside>
<GoogleBusinessMap
points={points}
center={center}
zoom={14}
onSelect={(point) => setSelectedId(point.id)}
/>
</section>
);
}
When Mapbox Is the Better Fit
Google Maps is strong for store locators, address search, familiar map data, and business-oriented location workflows. Mapbox GL JS is often a better fit when you own a large geospatial dataset, need custom cartography, or want WebGL-driven layers and animations. The official Mapbox GL JS guides explain the map, style, source, and layer model.
| Decision point | Google Maps | Mapbox GL JS |
|---|---|---|
| Store locator | Strong with Geocoding and Places | Good when your data is primary |
| Visual control | Cloud Styling is enough for most products | Very high style and layer control |
| Learning curve | Easier for common web apps | Requires understanding sources and layers |
| Operational risk | Key restrictions and billing alerts | Token restrictions and attribution |
// src/components/MapboxPreview.tsx
"use client";
import { useEffect, useRef } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
export function MapboxPreview() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
if (!containerRef.current || !token) return;
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({
container: containerRef.current,
style: "mapbox://styles/mapbox/streets-v12",
center: [-74.006, 40.7128],
zoom: 12,
});
map.addControl(new mapboxgl.NavigationControl(), "top-right");
return () => map.remove();
}, []);
return <div ref={containerRef} className="h-[420px] w-full rounded-lg border" />;
}
The practical instruction for Claude Code is not “build both.” It is “use Google Maps for address search and store discovery; use Mapbox only for the custom data layer.” That keeps the architecture understandable.
Three Practical Use Cases
The first use case is a store locator for branches, clinics, classes, showrooms, or event venues. The core workflow is address search, current-location fallback, hours, phone number, booking CTA, and mobile usability. In Masa’s prototype, the first version gave the map too much vertical space on mobile. The better version showed the list first and moved the map when the user selected a location.
The second use case is real estate or lodging search. Price, availability, walking time, room size, filters, and map bounds all need to stay in sync. Showing every marker is usually a mistake. Ask Claude Code to render only results inside the current viewport, cluster dense areas, and keep list sorting consistent with the visible map.
The third use case is delivery, field sales, or maintenance operations. Here the map is an operations surface, not a decorative page element. Decide how often locations update, who can see them, how long they are stored, and what happens when the device denies geolocation permission. If you add route optimization, review the Routes or Directions API cost model before writing code.
A fourth useful pattern is editorial mapping: travel articles, local guides, event reports, and neighborhood pages. A map can improve exploration, but it should not replace useful prose. Keep directions, local context, accessibility notes, and decision criteria in the article body.
Pitfalls to Catch Before Production
The first pitfall is an unrestricted API key. Browser keys are visible, but they should still be restricted by referrer and API. Server keys should never be shipped to the browser.
The second pitfall is copying old Marker examples. Google now points developers to Advanced Markers, and the official Advanced Markers guide shows the AdvancedMarkerElement flow. If Claude Code emits old code, ask it to modernize the component.
The third pitfall is SSR. In Next.js, any top-level use of google.maps.Map can fail with google is not defined. Keep the component client-only and load Maps inside useEffect.
The fourth pitfall is ambiguous geocoding. “Central Station” or “Main Street” may return multiple plausible places. Use language, region, country, and structured inputs where possible, and do not use raw addresses as database IDs.
The fifth pitfall is performance. Rendering hundreds of markers and list cards at once makes both the map and the page feel slow. For larger datasets, use clustering, viewport queries, pagination, or server-side search.
The sixth pitfall is privacy. Current location requires user permission. The UI must work when permission is denied, and any stored location data needs a clear purpose and retention rule.
Review Checklist for Claude Code
After Claude Code generates the implementation, review it critically:
- API keys are read from environment variables
- browser and server keys are separated
- Google Cloud API restrictions and application restrictions are documented
AdvancedMarkerElementis usedwindowandgoogleare never referenced during SSR- zero results, ambiguous addresses, and API failures have UI states
- user input is not injected into HTML strings
- mobile layout keeps both list and map usable
- billing alerts, quotas, and logs are part of the runbook
You can paste that checklist back into Claude Code and ask for a patch. That usually turns a demo map into something closer to production quality.
Summary and Verification Note
The safe way to integrate Google Maps with Claude Code is to split the job into map rendering, geocoding, key management, billing, privacy, and mobile UX. Google Maps is the default choice for store search and address workflows. Mapbox is strong when custom data layers and visual control are the main product value.
For this update, the code was checked for the parts that do not require a live provider key: Next.js responsibility boundaries, SSR-safe loading, code fence validity, and error branches. Before connecting a real key, configure Maps JavaScript API, Geocoding API, HTTP referrer restrictions, server-key restrictions, and budget alerts in Google Cloud Console. Start with three store records, verify search, marker click, mobile layout, and billing metrics, then expand to production data.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.