Google Integration & API Services

Google Maps Integration: Benefits, Features & Complete Business Guide

E encoderbaseAugust 2, 20265 min read
Google Maps Integration: Benefits, Features & Complete Business Guide

Build a Google Maps Location Tracker with Laravel 12 and Vue 3

1. Prerequisites

PHP 8.2+, Composer

Node.js 18+ and npm

A database (MySQL/SQLite — SQLite is fine for following along)

A Google Maps API key (see Step 2)

2. Get a Google Maps API key

Go to the Google Cloud Console.

Create a project (or select an existing one).

Enable these APIs: Maps JavaScript API, Places API (for address search), Geocoding API.

Go to APIs & Services → Credentials → Create Credentials → API Key.

Restrict the key: under Application restrictions, set HTTP referrers to your domain(s) (e.g. localhost:5173/*, yourdomain.com/*). Under API restrictions, limit it to the 3 APIs above.

Copy the key — you'll need it in Step 5.

Billing must be enabled on the Google Cloud project even for free-tier usage.

3. Create the Laravel 12 project with the Vue starter kit

Laravel 12's official starter kits scaffold Vue 3 + Inertia + Vite out of the box, which saves you from wiring up a separate SPA and CORS/auth by hand.

composer create-project laravel/laravel google-maps-app
cd google-maps-app

# Install the Vue starter kit (installs Inertia, Vue 3, Vite, Tailwind)
php artisan install:api        # optional, if you also want a token-based API
composer require laravel/breeze --dev
php artisan breeze:install vue

If you're using laravel new via the Laravel installer instead, you can pick "Vue" directly when prompted for a starter kit — same result.

Install JS dependencies and confirm the dev server runs:

npm install
npm run dev

In another terminal:

php artisan migrate
php artisan serve

Visit http://localhost:8000 — you should see the default Breeze/Vue welcome or login page.

4. Database: locations table + model

Create a migration:

php artisan make:model Location -mcr

This gives you Location.php, a migration, a controller, and a resource-style route stub.

database/migrations/xxxx_create_locations_table.php

public function up(): void
{
    Schema::create('locations', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->decimal('lat', 10, 7);
        $table->decimal('lng', 10, 7);
        $table->timestamps();
    });
}

Run it:

php artisan migrate

app/Models/Location.php

class Location extends Model
{
    protected $fillable = ['name', 'description', 'lat', 'lng'];

    protected $casts = [
        'lat' => 'float',
        'lng' => 'float',
    ];
}

5. API routes and controller

routes/api.php

use App\Http\Controllers\LocationController;

Route::apiResource('locations', LocationController::class);

Make sure bootstrap/app.php has the API routes registered (Laravel 12 does this by default if you ran php artisan install:api; otherwise add):

->withRouting(
    web: __DIR__.'/../routes/web.php',
    api: __DIR__.'/../routes/api.php',
    commands: __DIR__.'/../routes/console.php',
    health: '/up',
)

app/Http/Controllers/LocationController.php

namespace App\Http\Controllers;

use App\Models\Location;
use Illuminate\Http\Request;

class LocationController extends Controller
{
    public function index()
    {
        return Location::orderByDesc('id')->get();
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'        => 'required|string|max:255',
            'description' => 'nullable|string',
            'lat'         => 'required|numeric|between:-90,90',
            'lng'         => 'required|numeric|between:-180,180',
        ]);

        $location = Location::create($validated);

        return response()->json($location, 201);
    }

    public function destroy(Location $location)
    {
        $location->delete();

        return response()->noContent();
    }
}

Quick test:

curl http://localhost:8000/api/locations

Should return [] (empty array) since the table is fresh.

6. Install the Google Maps package for Vue 3

npm install vue3-google-map

7. Add the API key to .env

.env

VITE_GOOGLE_MAPS_API_KEY=your_api_key_here

Vite only exposes env vars prefixed with VITE_ to client-side code — this is intentional and required.

8. Build the Map component

resources/js/Components/LocationMap.vue

<script setup>
import { ref, onMounted } from 'vue'
import { GoogleMap, AdvancedMarker, InfoWindow } from 'vue3-google-map'
import axios from 'axios'

const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY

const center = ref({ lat: 23.8103, lng: 90.4125 }) // default: Dhaka
const locations = ref([])
const selected = ref(null)
const newLocationName = ref('')
const pendingClick = ref(null)

async function fetchLocations() {
  const { data } = await axios.get('/api/locations')
  locations.value = data
}

function handleMapClick(event) {
  pendingClick.value = {
    lat: event.latLng.lat(),
    lng: event.latLng.lng(),
  }
}

async function saveLocation() {
  if (!pendingClick.value || !newLocationName.value) return

  const { data } = await axios.post('/api/locations', {
    name: newLocationName.value,
    lat: pendingClick.value.lat,
    lng: pendingClick.value.lng,
  })

  locations.value.unshift(data)
  newLocationName.value = ''
  pendingClick.value = null
}

async function removeLocation(id) {
  await axios.delete(`/api/locations/${id}`)
  locations.value = locations.value.filter((l) => l.id !== id)
  selected.value = null
}

onMounted(fetchLocations)
</script>

<template>
  <div class="space-y-4">
    <!-- Add-marker form, shown after a map click -->
    <div v-if="pendingClick" class="flex gap-2 items-center p-3 border rounded-md bg-gray-50">
      <span class="text-sm text-gray-600">
        {{ pendingClick.lat.toFixed(5) }}, {{ pendingClick.lng.toFixed(5) }}
      </span>
      <input
        v-model="newLocationName"
        type="text"
        placeholder="Location name"
        class="border rounded px-2 py-1 text-sm flex-1"
      />
      <button
        @click="saveLocation"
        class="bg-blue-600 text-white text-sm px-3 py-1 rounded"
      >
        Save
      </button>
      <button
        @click="pendingClick = null"
        class="text-sm text-gray-500 px-2"
      >
        Cancel
      </button>
    </div>

    <GoogleMap
      :api-key="apiKey"
      map-id="DEMO_MAP_ID"
      style="width: 100%; height: 500px"
      :center="center"
      :zoom="12"
      @click="handleMapClick"
    >
      <AdvancedMarker
        v-for="loc in locations"
        :key="loc.id"
        :options="{ position: { lat: loc.lat, lng: loc.lng }, title: loc.name }"
        @click="selected = loc"
      />

      <InfoWindow
        v-if="selected"
        :options="{ position: { lat: selected.lat, lng: selected.lng } }"
        @closeclick="selected = null"
      >
        <div class="p-1">
          <p class="font-semibold">{{ selected.name }}</p>
          <p v-if="selected.description" class="text-sm text-gray-600">
            {{ selected.description }}
          </p>
          <button
            @click="removeLocation(selected.id)"
            class="text-red-600 text-xs mt-1 underline"
          >
            Delete
          </button>
        </div>
      </InfoWindow>
    </GoogleMap>
  </div>
</template>

Key points:

map-id="DEMO_MAP_ID" is required for AdvancedMarker to render (Google's newer marker API needs a Map ID — you can create a real one in Cloud Console under Map Management for custom styling, or use DEMO_MAP_ID for development).

Clicking the map captures lat/lng, then the small form posts it to your Laravel API.

Markers are rendered directly from the locations array fetched from /api/locations.

9. Use the component in a page

resources/js/Pages/Dashboard.vue (or any page)

<script setup>
import LocationMap from '@/Components/LocationMap.vue'
</script>

<template>
  <div class="max-w-4xl mx-auto py-8">
    <h1 class="text-xl font-semibold mb-4">Locations Map</h1>
    <LocationMap />
  </div>
</template>

10. Run everything

# Terminal 1
php artisan serve

# Terminal 2
npm run dev

Visit the page — click anywhere on the map, name the spot, hit Save, and it persists to the database and reappears on refresh.

11. Optional: address search with Places Autocomplete

If you'd rather type an address than click the map, add an input using the Places library:

npm install @googlemaps/js-api-loader
<script setup>
import { ref, onMounted } from 'vue'

const searchInput = ref(null)
const emit = defineEmits(['place-selected'])

onMounted(() => {
  const autocomplete = new google.maps.places.Autocomplete(searchInput.value)
  autocomplete.addListener('place_changed', () => {
    const place = autocomplete.getPlace()
    if (place.geometry) {
      emit('place-selected', {
        lat: place.geometry.location.lat(),
        lng: place.geometry.location.lng(),
        name: place.name,
      })
    }
  })
})
</script>

<template>
  <input ref="searchInput" type="text" placeholder="Search an address..." class="border rounded px-3 py-2 w-full" />
</template>

Make sure libraries: ['places'] is passed as a prop to the GoogleMap component (:libraries="['places']") so the Places library loads alongside the map.

Share this article
E
encoderbase

Insights and guides from the Encoderbase team on web, apps, software and SEO.

Have a project in mind?

Let's build something great together.

Get in Touch