店舗・物件・工事現場・点検箇所など、「場所に紐づく情報」がExcelや紙の台帳に散らばっていて、どこに何があるのかを一目で見られない。そんな悩みを、Google Maps API(Google Maps Platform)とNuxtを使った社内向けの地図アプリで解決していく連載を始めます。
「Google Mapsを使った社内システムを作りたいけれど、APIキーや料金の仕組みがよく分からない。まず何から始めればいいの?」
結論から言うと、最初にやることは3つだけです。Google Cloud のプロジェクトと請求先を発注者の名義で用意し、制限付きのAPIキーを発行して、Nuxtから地図を1枚表示する。この回では、連載全体の見取り図を紹介したうえで、この「地図1枚」までを動くコード付きで解説します。
連載「NuxtとGoogle Mapsで作る情報集約マップ」の全体像
この連載では、地図の上に社内の情報を集約して、登録・検索・絞り込み・共有できるWebアプリを、1回1機能ずつ作っていきます。最終的には、そのまま社内の試作(PoC=本格導入前の小さな検証)に使えるサンプル一式ができあがります。
想定読者
| 読者 | この連載で得られること |
|---|---|
| 発注側の社内エンジニア・情シス担当 | 小さく自作する、または外注先の成果物をレビューできるようになる |
| Vue/Nuxt経験のあるフロントエンド開発者 | Google Maps Platform の最新の作法(Advanced Markers、Places API (New)、新しい料金体系)を押さえる |
| 技術に関心のある発注者 | 機能ごとの工数感・API費用の発生の仕方・リスクを掴み、見積りや要件定義の会話ができる |
使う技術
- Nuxt 4(Vue 3 + TypeScript + Composition API)
- @googlemaps/js-api-loader 2(Google公式の読み込みライブラリ)
- Maps JavaScript API(地図表示)、Places API (New)(施設検索)、Geocoding API(住所→緯度経度)
- データ保存は SQLite(Node.js 24 組み込みの
node:sqlite)。本番では PostgreSQL などに差し替える前提
地図の描画はブラウザだけで行い、サーバー側(Nuxt の server API)は地点データの保存と、秘密にすべきキーを使う処理(住所の変換)を担当します。
全12回の目次(予定)
| 回 | タイトル(案) |
|---|---|
| 第1回 | APIキーとMap IDを用意して、Nuxt 4で地図を1枚表示する(この記事) |
| 第2回 | Advanced Markersでカテゴリ別の色付きピンを立てる |
| 第3回 | 地点データをNuxt server API + SQLiteに保存する |
| 第4回 | マーカーをクリックしてInfoWindowと詳細パネルを出す |
| 第5回 | 地図をクリックして新しい地点を登録する |
| 第6回 | 住所を入力して緯度経度に変換する(Geocoding APIをサーバー経由で) |
| 第7回 | 施設名で検索して地点を取り込む(Places API (New)) |
| 第8回 | カテゴリで絞り込み、一覧と地図を連動させる |
| 第9回 | マーカーが数千件でも重くしない:クラスタリングと表示範囲読み込み |
| 第10回 | 現在地から半径◯km/描いたエリア内の地点を探す |
| 第11回 | CSVで既存の台帳を一括取り込みする |
| 第12回 | 本番公開前チェック:APIキー制限・クォータ・予算アラート・性能 |
各回の記事には「発注者向けメモ」として、その機能を開発会社に頼むときの確認点や、工数・API費用が増える条件をまとめます(金額は改定が多いため書かず、費用が発生する「仕組み」を説明します)。
Google Maps APIで地図を表示する仕組み
コードの前に、登場する用語を整理しておきます。
| 用語 | 意味 |
|---|---|
| Google Cloud プロジェクト | APIの利用・課金・キーをまとめる「入れ物」 |
| 請求先アカウント | 利用料の支払い元。Maps JavaScript API の利用には請求先の設定が必要 |
| APIキー | 「どのプロジェクトの利用か」を示す文字列。ブラウザで使うキーは利用者から見える前提で扱う |
| Map ID | 地図の設定(スタイル等)を識別するID。第2回で使う Advanced Markers に必須。開発中は DEMO_MAP_ID で代用できる |
| マップロード | 地図を1回読み込むこと。地図表示(Dynamic Maps)はこの単位で課金される |
📰 出典:Google Maps Platform「Maps JavaScript API Usage and Billing」
公式の料金説明では、Maps JavaScript API の地図読み込み(map load)が Dynamic Maps という料金項目(SKU)の対象で、区分は Essentials とされています。2025年3月の料金体系の変更で、以前の「毎月200ドル分の無料クレジット」は、料金項目ごとの月間無料枠に置き換わりました。具体的な単価や無料枠は公式の料金ページで確認してください。
📰 出典:Google Maps Platform「March 2025 changes」
Google Cloudでの準備(APIキーとMap ID)
Cloud コンソールで次の順に準備します(画面の文言は執筆時点・2026年9月のもの)。
- プロジェクトを作成し、請求先アカウントを紐づける
- 「APIとサービス」で Maps JavaScript API を有効にする(Places API (New)、Geocoding API は使う回で有効化します)
- 「認証情報」で APIキーを作成し、すぐに制限を設定する
- アプリケーションの制限: ウェブサイト(HTTPリファラー)。開発中は
http://localhost:3000/*を登録 - APIの制限: Maps JavaScript API のみに絞る
- Map ID は、開発中は
DEMO_MAP_IDのままで構いません。本番では「Map Management(マップ管理)」画面で JavaScript 用の Map ID を作成します
📰 出典:Google Maps Platform「API security best practices」
ブラウザで使うキーは、画面のソースを見れば誰でも読めます。「隠す」のではなく、リファラー制限とAPI制限で「このサイトから、この用途にしか使えない」ようにするのが公式の考え方です。住所変換などサーバーから呼ぶAPIには、第6回で別のキーを発行します。
📰 出典:Google Maps Platform「Use Map IDs」
実装:Nuxt 4で地図を1枚表示する
第0回相当の骨格(Nuxt 4 のプロジェクト、runtimeConfig の定義、.env.example)はサンプルに用意済みです。この回で追加・変更したファイルは次のとおりです。
| ファイル | 役割 |
|---|---|
app/plugins/google-maps.client.ts | Maps JavaScript API の読み込み設定(1回だけ) |
app/composables/useGoogleMaps.ts | ライブラリ読み込みとエラー状態をまとめる |
app/components/MapView.vue | 地図を表示するコンポーネント |
app/pages/index.vue | <ClientOnly> で地図を配置 |
app/types/google-maps.d.ts | google.maps.* の型を有効にする |
手順1:ライブラリを追加する
npm install @googlemaps/js-api-loader
npm install -D @types/google.maps
@googlemaps/js-api-loader は Google 公式の読み込みライブラリで、バージョン2では setOptions() と importLibrary() の2つの関数だけを使います(旧バージョンの Loader クラスは非推奨)。コミュニティ製のVueラッパー(vue3-google-map)もありますが、この連載は公式ドキュメントのサンプルとそのまま対応が取れること、数千件のマーカーを細かく制御することを重視して、公式ローダーを薄い composable で包む方針にしました。
📰 出典:@googlemaps/js-api-loader(npm)
手順2:キーは runtimeConfig と .env で渡す
nuxt.config.ts(抜粋)では、キーの既定値を空文字にしておき、値は環境変数で上書きします。ソースコードにキーを書かないためです。
// nuxt.config.ts(抜粋)
runtimeConfig: {
googleMapsServerKey: '', // サーバー専用(第6回で使用)
public: {
googleMapsApiKey: '', // ← NUXT_PUBLIC_GOOGLE_MAPS_API_KEY
googleMapsMapId: 'DEMO_MAP_ID', // ← NUXT_PUBLIC_GOOGLE_MAPS_MAP_ID
},
},
.env.example をコピーして .env を作り、発行したキーを設定します。.env は .gitignore 済みで、リポジトリには入りません。
cp .env.example .env
# .env の NUXT_PUBLIC_GOOGLE_MAPS_API_KEY= に自分のキーを設定
📰 出典:Nuxt「Runtime Config」
注意点として、npm run dev では .env が読まれますが、ビルド後の本番サーバー(node .output/server/index.mjs)では .env は読まれません。本番では実行環境の環境変数として渡します。
手順3:読み込み設定はプラグインで1回だけ
app/plugins/google-maps.client.ts
import { setOptions } from '@googlemaps/js-api-loader'
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const apiKey = config.public.googleMapsApiKey
// キー未設定なら読み込み設定をしない(useGoogleMaps 側で画面にエラーを出す)
if (!apiKey) return
setOptions({
key: apiKey,
v: 'weekly',
language: 'ja',
region: 'JP',
})
// キーの無効・リファラー制限違反などの認証エラー時に Google が呼ぶグローバル関数
window.gm_authFailure = () => {
useState<boolean>('gmaps-auth-failed').value = true
}
})
ファイル名の .client で、このプラグインはブラウザでのみ実行されます。setOptions() はアプリ全体で1回だけ呼ぶ設計です(2回目以降は効果がなく、コンソールに警告が出る仕様)。gm_authFailure は、認証に失敗したときに Maps JavaScript API が呼び出すグローバル関数で、公式ドキュメントで案内されている検知方法です。
📰 出典:Google Maps Platform「Events – Listen for authentication errors」
手順4:composableで読み込みとエラー状態をまとめる
app/composables/useGoogleMaps.ts
import { importLibrary } from '@googlemaps/js-api-loader'
type LibraryName = keyof google.maps.ImportLibraryMap
export function useGoogleMaps() {
const config = useRuntimeConfig()
const isConfigured = computed(() => config.public.googleMapsApiKey.length > 0)
const mapId = config.public.googleMapsMapId || 'DEMO_MAP_ID'
// plugins/google-maps.client.ts の gm_authFailure から true にされる
const authFailed = useState<boolean>('gmaps-auth-failed', () => false)
async function load<T extends LibraryName>(name: T) {
if (!isConfigured.value) {
throw new Error('NUXT_PUBLIC_GOOGLE_MAPS_API_KEY が未設定です(.env を確認してください)')
}
return importLibrary(name)
}
return { isConfigured, mapId, authFailed, load }
}
importLibrary('maps') のように必要なライブラリを必要なときに読み込みます。同じライブラリなら何度呼んでも同じ結果が返るので、今後の回でマーカー(marker)や施設検索(places)を使うときも、この load() を呼ぶだけです。
google.maps.* の型を使うため、app/types/google-maps.d.ts に /// <reference types="google.maps" /> の1行を置いています。これがないと nuxt typecheck で「namespace ‘google’ が見つからない」エラーになります。
手順5:地図を表示するコンポーネント
app/components/MapView.vue(script 部分)
<script setup lang="ts">
const props = withDefaults(
defineProps<{ center?: google.maps.LatLngLiteral; zoom?: number }>(),
{
center: () => ({ lat: 35.681236, lng: 139.767125 }), // 東京駅
zoom: 15,
},
)
const emit = defineEmits<{ ready: [map: google.maps.Map] }>()
const { isConfigured, mapId, authFailed, load } = useGoogleMaps()
const mapEl = ref<HTMLDivElement | null>(null)
// 地図インスタンスは shallowRef で持つ(ref だと Vue が内部を深く監視して重くなる)
const map = shallowRef<google.maps.Map | null>(null)
const loadError = ref<string | null>(null)
onMounted(async () => {
if (!isConfigured.value || !mapEl.value) return
try {
const { Map } = await load('maps')
map.value = new Map(mapEl.value, {
center: props.center,
zoom: props.zoom,
mapId, // Advanced Markers(第2回)に必須
gestureHandling: 'greedy',
clickableIcons: false,
})
emit('ready', map.value)
} catch (e) {
loadError.value = e instanceof Error ? e.message : String(e)
}
})
defineExpose({ map })
</script>
テンプレート側では、地図を描く div の上に「キー未設定」「認証エラー」「読み込み失敗」のメッセージを重ねて表示します(全文はサンプルの MapView.vue を参照)。キーが未設定のまま地図が真っ白、という状態を避け、何を直せばいいかを画面に出すのがポイントです。
地図のインスタンスを ref ではなく shallowRef で持つのは、Vue が地図オブジェクトの内部まで変更監視しようとして重くなるのを避けるためです。今後マーカーが数千件になる回でも同じ方針を続けます。
手順6:ページに <ClientOnly> で置く
app/pages/index.vue
<template>
<ClientOnly>
<MapView />
<template #fallback>
<div class="map-placeholder">地図を読み込み中…</div>
</template>
</ClientOnly>
</template>
Nuxt はサーバー側でHTMLを作ってから(SSR)ブラウザに送ります。地図はブラウザにしか描けないため、<ClientOnly> で囲み、サーバー側では「読み込み中」の枠だけを返します。
動作確認の方法
.envにブラウザ用キーを設定し、npm run dev→ http://localhost:3000 を開く- 東京駅を中心に地図が表示され、DevTools のコンソールに
InvalidKeyMapErrorやRefererNotAllowedMapErrorなどのエラーが出ていないことを確認 .envのキーを空にして再起動すると、地図の代わりに「ブラウザ用APIキーが未設定です」と表示されることを確認npm run buildとnpm run typecheckが通ることを確認
筆者の環境では、npm run build と npm run typecheck の成功、ビルド後のサーバーが返すHTMLに「地図を読み込み中…」の枠が入ること、ブラウザに渡る設定値にサーバー専用キーが含まれないこと(ダミー値で確認)までを確かめています。実際の地図の描画は、執筆環境に本物のAPIキーがないため確認できていません。 ご自身のキーを .env に設定して確認してください。
📰 出典:Google Maps Platform「Error Messages」
よく出るエラーと原因は次のとおりです。
| エラー | 主な原因 |
|---|---|
ApiNotActivatedMapError | プロジェクトで Maps JavaScript API が有効になっていない |
InvalidKeyMapError | キーの値が間違っている |
RefererNotAllowedMapError | 開いているURLがキーのリファラー制限に登録されていない(localhost:3000 と 127.0.0.1:3000 は別扱い) |
BillingNotEnabledMapError | プロジェクトに請求先が設定されていない |
ApiTargetBlockedMapError | キーのAPI制限で Maps JavaScript API が許可されていない |
地図が暗くなり「for development purposes only」と表示される場合も、キーか請求先の設定に問題があるサインです。
つまずきやすい点・セキュリティ上の注意
- キーを制限なしのまま放置しない。発行した直後に、リファラー制限とAPI制限を設定します
- キーをコミットしない。
.envは.gitignore済みです。スクリーンショットや記事にもキーの値を載せないため、サンプルの画面はキーの「有無」だけを表示しています - ブラウザ用キーとサーバー用キーを分ける。ブラウザ用キーは公開される前提なので、住所変換などに流用しません(第6回)
- 予算アラートを先に設定する。Cloud コンソールの「お支払い」で予算とアラートを設定し、API ごとの上限(クォータ)も確認しておくと、想定外の利用に早く気づけます(第12回で総点検します)
発注者向けメモ
- Google Cloud のプロジェクト・請求先・APIキーは、原則として発注者の名義で作る。開発会社や担当者個人のアカウントで作ると、契約終了や退職のときにキーを止められない・請求を引き継げない、といった事態になりがちです。開発会社には「発注者のプロジェクトに権限を付与して作業してもらう」形を相談しましょう
- 地図の費用は「表示回数(マップロード)」で決まる。社内の数人が使うのか、一般公開して多数が見るのかで、同じ機能でも費用がまったく変わります。想定利用者数と1日の利用頻度を見積り前に伝えてください
- キー制限・予算アラートは「作業項目」として見積りに入っているかを確認します。後回しにされやすいですが、漏えい時の被害を小さくする重要な設定です
開発会社への確認に使える質問例です。
- 「Google Cloud のプロジェクトと請求先は、どちらの名義で作る想定ですか?」
- 「ブラウザ用のAPIキーには、どのような制限を設定しますか?」
- 「予算アラートとAPIごとの利用上限は、誰がどの値で設定しますか?」
まとめと次回予告
この回では、連載の全体像と、Google Cloud でのキー・Map ID の準備、Nuxt 4 で地図を1枚表示するまでを解説しました。ポイントは、読み込み設定を1か所(クライアント専用プラグイン)にまとめること、キーは runtimeConfig と .env で扱いコードに書かないこと、エラーを画面に出して原因を分かりやすくすることの3つです。
次回は「Advanced Markersでカテゴリ別の色付きピンを立てる」です。店舗・物件・現場・点検の4カテゴリのサンプル地点20件を、非推奨になった旧 Marker ではなく Advanced Markers で色分け表示します。
この連載の記事一覧
この記事は連載「NuxtとGoogle Mapsで作る情報集約マップ」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【NuxtとGoogle Mapsで作る情報集約マップ 第1回】APIキーとMap IDを用意して、Nuxt 4で地図を1枚表示する(この記事)
- 【NuxtとGoogle Mapsで作る情報集約マップ 第2回】Advanced Markersでカテゴリ別の色付きピンを立てる
- 【NuxtとGoogle Mapsで作る情報集約マップ 第3回】地点データをNuxt server API + SQLiteに保存する
- 【NuxtとGoogle Mapsで作る情報集約マップ 第4回】マーカーをクリックしてInfoWindowと詳細パネルを出す
- 【NuxtとGoogle Mapsで作る情報集約マップ 第5回】地図をクリックして新しい地点を登録する
- 【NuxtとGoogle Mapsで作る情報集約マップ 第6回】住所を入力して緯度経度に変換する(Geocoding API v4をサーバー経由で)
- 【NuxtとGoogle Mapsで作る情報集約マップ 第7回】施設名で検索して地点を取り込む(Places API (New))
- 【NuxtとGoogle Mapsで作る情報集約マップ 第8回】カテゴリで絞り込み、一覧と地図を連動させる
- 【NuxtとGoogle Mapsで作る情報集約マップ 第9回】マーカーが数千件でも重くしない:クラスタリングと表示範囲読み込み
- 【NuxtとGoogle Mapsで作る情報集約マップ 第10回】現在地から半径◯km/描いたエリア内の地点を探す
- 【NuxtとGoogle Mapsで作る情報集約マップ 第11回】CSVで既存の台帳を一括取り込みする
- 【NuxtとGoogle Mapsで作る情報集約マップ 第12回】本番公開前チェック:APIキー制限・クォータ・予算アラート・性能










コメント
コメント一覧 (1件)
[…] Maps API で社内向けの情報集約マップを作る連載の第2回です。前回(第1回)では、APIキーとMap IDを用意して、Nuxt 4 […]