Google Maps Routes API移行ガイド【Directions API完全対応】
はじめに
2025年3月、GoogleはDirections API / Distance Matrix APIをレガシー指定しました。今後はRoutes API (computeRoutes, computeRouteMatrix) の使用が必須となります。
この記事では、実際のプロジェクトでRoutes APIへの移行を実施した際の実装例と、移行時に直面した課題と解決策を詳しく解説します。
想定読者
- Directions API / Distance Matrix APIを使用している方
- Routes APIへの移行を検討している方
- Google Maps APIを使ったルート最適化機能を実装したい方
この記事で学べること
- ✅ Directions API → Routes API (
computeRoutes) の移行方法 - ✅ Distance Matrix API → Routes API (
computeRouteMatrix) の移行方法(ストリーミング対応) - ✅ Routes API共通クライアントの設計パターン
- ✅ フィールドマスク・エラーハンドリングの実装方法
前提条件
この記事の内容を実践するには、以下の環境が必要です:
- Node.js 18.x 以上
- Google Maps API キー(Routes API有効化済み)
- TypeScript 5.x 以上
# 環境確認
node -v
# v18.x 以上
なぜRoutes APIに移行する必要があるのか
レガシー指定の背景
Googleは2025年3月に以下のAPIをレガシー指定しました:
-
Directions API (
/maps/api/directions/json) -
Distance Matrix API (
/maps/api/distancematrix/json)
今後はRoutes APIの使用が推奨されます:
-
computeRoutes (
/routes.googleapis.com/directions/v2:computeRoutes) -
computeRouteMatrix (
/routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix)
主な変更点
| 項目 | 旧API | Routes API |
|---|---|---|
| HTTPメソッド | GET | POST |
| 認証 | Query param ?key=API_KEY
|
Header X-Goog-Api-Key
|
| フィールドマスク | なし(全フィールド返却) |
必須(X-Goog-FieldMask) |
| レスポンス形式 | JSON(同期) | JSON(同期)/ ストリーミング |
| 座標形式 | "37.419734,-122.0827784" |
{latLng: {latitude: 37.419734, longitude: -122.0827784}} |
Routes API 共通クライアントの実装
まず、computeRoutes と computeRouteMatrix の共通処理を提供するクライアントクラスを実装します。
RoutesClient クラス
/**
* Routes API 共通クライアント
* computeRoutes と computeRouteMatrix の共通処理を提供
*/
export interface RoutesClientConfig {
apiKey: string;
baseUrl?: string;
}
export class RoutesClient {
private apiKey: string;
private baseUrl: string;
constructor(config: RoutesClientConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://routes.googleapis.com';
}
/**
* 共通ヘッダーを生成
* X-Goog-Api-Key と X-Goog-FieldMask は必須
*/
private getHeaders(fieldMask: string): Headers {
return new Headers({
'Content-Type': 'application/json',
'X-Goog-Api-Key': this.apiKey, // ← Query paramではなくHeader
'X-Goog-FieldMask': fieldMask, // ← 必須!
});
}
/**
* computeRoutes エンドポイント呼び出し
*/
async computeRoutes(
request: ComputeRoutesRequest,
fieldMask?: string
): Promise<ComputeRoutesResponse> {
// デフォルトのフィールドマスク
const defaultFieldMask =
'routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline,routes.legs,routes.optimizedIntermediateWaypointIndex';
const mask = fieldMask || defaultFieldMask;
try {
const response = await fetch(`${this.baseUrl}/directions/v2:computeRoutes`, {
method: 'POST', // ← GETではなくPOST
headers: this.getHeaders(mask),
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Routes API error (${errorData.error.code}): ${errorData.error.message}`
);
}
const data: ComputeRoutesResponse = await response.json();
if (!data.routes || data.routes.length === 0) {
throw new Error('指定された地点間のルートが見つかりませんでした');
}
return data;
} catch (error: any) {
throw new Error(`ルート計算に失敗しました: ${error.message}`);
}
}
/**
* Waypoint を LatLng 座標から生成
*/
static createWaypointFromLatLng(lat: number, lng: number) {
return {
location: {
latLng: {
latitude: lat,
longitude: lng,
},
},
};
}
/**
* Duration 文字列を秒数に変換
* @param duration "123s" 形式の文字列
*/
static parseDuration(duration: string): number {
return parseInt(duration.replace('s', ''), 10);
}
}
型定義
/**
* Routes API: computeRoutes Request
*/
export interface ComputeRoutesRequest {
origin: Waypoint;
destination: Waypoint;
intermediates?: Waypoint[];
travelMode?: 'DRIVE' | 'WALK' | 'BICYCLE' | 'TRANSIT';
routingPreference?: 'TRAFFIC_UNAWARE' | 'TRAFFIC_AWARE' | 'TRAFFIC_AWARE_OPTIMAL';
polylineQuality?: 'HIGH_QUALITY' | 'OVERVIEW';
polylineEncoding?: 'ENCODED_POLYLINE' | 'GEO_JSON_LINESTRING';
departureTime?: string; // ISO 8601
computeAlternativeRoutes?: boolean;
languageCode?: string;
units?: 'IMPERIAL' | 'METRIC';
optimizeWaypointOrder?: boolean; // ← 簡易最適化用
}
export interface Waypoint {
location?: {
latLng?: {
latitude: number;
longitude: number;
};
};
placeId?: string;
address?: string;
}
export interface ComputeRoutesResponse {
routes: Route[];
}
export interface Route {
legs: RouteLeg[];
distanceMeters: number;
duration: string; // "123s"
polyline: {
encodedPolyline: string;
};
optimizedIntermediateWaypointIndex?: number[]; // ← 最適化後の順序
}
export interface RouteLeg {
distanceMeters: number;
duration: string; // "123s"
startLocation: {
latLng?: {
latitude: number;
longitude: number;
};
};
endLocation: {
latLng?: {
latitude: number;
longitude: number;
};
};
}
ルート最適化の実装(computeRoutes)
Directions APIの optimize:true に相当する機能を Routes API で実装します。
optimizeRoute 関数
/**
* ルート最適化(Routes API computeRoutes使用)
*
* @param waypoints 地点の座標配列(2地点以上)
* @param travelMode 移動手段
* @returns 最適化結果
*/
export async function optimizeRoute(
waypoints: Array<{ lat: number; lng: number }>,
travelMode: 'DRIVING' | 'WALKING' | 'BICYCLING' = 'DRIVING'
): Promise<OptimizationResult> {
if (waypoints.length < 2) {
throw new Error('Waypoints must have at least 2 points');
}
// Routes API クライアント初期化
const client = new RoutesClient({
apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ''
});
// travelMode を Routes API 形式に変換
// DRIVING → DRIVE, WALKING → WALK, BICYCLING → BICYCLE
const travelModeMap: Record<string, 'DRIVE' | 'WALK' | 'BICYCLE'> = {
DRIVING: 'DRIVE',
WALKING: 'WALK',
BICYCLING: 'BICYCLE',
};
// リクエスト構築
const request: ComputeRoutesRequest = {
origin: RoutesClient.createWaypointFromLatLng(
waypoints[0].lat,
waypoints[0].lng
),
destination: RoutesClient.createWaypointFromLatLng(
waypoints[waypoints.length - 1].lat,
waypoints[waypoints.length - 1].lng
),
intermediates: waypoints.length > 2
? waypoints
.slice(1, -1)
.map((wp) => RoutesClient.createWaypointFromLatLng(wp.lat, wp.lng))
: undefined,
travelMode: travelModeMap[travelMode] || 'DRIVE',
optimizeWaypointOrder: true, // ← 簡易最適化を有効化
computeAlternativeRoutes: false,
languageCode: 'ja',
units: 'METRIC',
};
// computeRoutes 呼び出し
const response = await client.computeRoutes(request);
if (!response.routes || response.routes.length === 0) {
throw new Error('指定された地点間のルートが見つかりませんでした');
}
const route = response.routes[0];
// 最適化後の地点順序を取得
const optimizedIntermediateIndices = route.optimizedIntermediateWaypointIndex || [];
const fullOrder = [
0, // 出発地
...optimizedIntermediateIndices.map((idx: number) => idx + 1), // 中間地点
waypoints.length - 1, // 目的地
];
// 総距離・総所要時間を計算
const totalDistance = route.distanceMeters;
const totalDuration = RoutesClient.parseDuration(route.duration);
return {
waypointOrder: fullOrder,
totalDistance,
totalDuration,
polyline: route.polyline.encodedPolyline || '',
};
}
戻り値の型定義:
export interface OptimizationResult {
/** 最適化後の地点順序(元の配列のインデックス) */
waypointOrder: number[];
/** 総距離(メートル) */
totalDistance: number;
/** 総所要時間(秒) */
totalDuration: number;
/** ポリライン */
polyline: string;
}
Directions API との比較
変更前(Directions API):
// GET リクエスト(Query param)
const url = new URL('https://maps.googleapis.com/maps/api/directions/json');
url.searchParams.append('origin', `${waypoints[0].lat},${waypoints[0].lng}`);
url.searchParams.append('destination', `${waypoints[waypoints.length - 1].lat},${waypoints[waypoints.length - 1].lng}`);
url.searchParams.append('waypoints', `optimize:true|${waypointsParam}`);
url.searchParams.append('key', API_KEY);
const response = await fetch(url.toString());
変更後(Routes API):
// POST リクエスト(JSON Body)
const request: ComputeRoutesRequest = {
origin: RoutesClient.createWaypointFromLatLng(waypoints[0].lat, waypoints[0].lng),
destination: RoutesClient.createWaypointFromLatLng(waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].lng),
intermediates: waypoints.slice(1, -1).map((wp) =>
RoutesClient.createWaypointFromLatLng(wp.lat, wp.lng)
),
travelMode: 'DRIVE',
optimizeWaypointOrder: true, // ← optimize:true の代わり
};
const response = await client.computeRoutes(request);
距離行列の取得(computeRouteMatrix)
Distance Matrix APIの代替として、Routes API の computeRouteMatrix を使用します。複数地点間の距離・時間を一度に取得できます。
重要:ストリーミングレスポンス
computeRouteMatrix はServer-sent events(ストリーミング)形式でレスポンスを返します。
/**
* 複数地点間の距離・時間を取得(Routes API computeRouteMatrix)
* ストリーミングレスポンスを処理
*/
export async function getDistanceMatrix(
origins: Array<{ lat: number; lng: number }>,
destinations: Array<{ lat: number; lng: number }>,
travelMode: 'DRIVING' | 'WALKING' | 'BICYCLING' = 'DRIVING'
): Promise<DistanceMatrixResult> {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new Error('Google Maps API key is not configured');
}
// 地点数の制限チェック(Routes APIは最大25x25)
if (origins.length > 25 || destinations.length > 25) {
throw new Error('地点数は最大25件までです');
}
// Routes API クライアント初期化
const client = new RoutesClient({ apiKey });
// travelMode を Routes API 形式に変換
const travelModeMap: Record<string, 'DRIVE' | 'WALK' | 'BICYCLE'> = {
DRIVING: 'DRIVE',
WALKING: 'WALK',
BICYCLING: 'BICYCLE',
};
// リクエスト構築
const request: ComputeRouteMatrixRequest = {
origins: origins.map((o) => ({
waypoint: RoutesClient.createWaypointFromLatLng(o.lat, o.lng),
})),
destinations: destinations.map((d) => ({
waypoint: RoutesClient.createWaypointFromLatLng(d.lat, d.lng),
})),
travelMode: travelModeMap[travelMode] || 'DRIVE',
languageCode: 'ja',
units: 'METRIC',
};
// computeRouteMatrix 呼び出し(ストリーミング)
const elements: RouteMatrixElement[] = await client.computeRouteMatrix(request);
// 距離行列を構築
const matrix = buildDistanceMatrix(elements, origins.length, destinations.length);
return { matrix };
}
ストリーミングレスポンスの処理
/**
* computeRouteMatrix エンドポイント呼び出し(ストリーミング)
*/
async computeRouteMatrix(
request: ComputeRouteMatrixRequest,
fieldMask?: string
): Promise<RouteMatrixElement[]> {
const defaultFieldMask = 'originIndex,destinationIndex,distanceMeters,duration,condition';
const mask = fieldMask || defaultFieldMask;
try {
const response = await fetch(`${this.baseUrl}/distanceMatrix/v2:computeRouteMatrix`, {
method: 'POST',
headers: this.getHeaders(mask),
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Routes API error (${errorData.error.code}): ${errorData.error.message}`
);
}
// ストリーミングレスポンスの処理
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Failed to get response stream');
}
const decoder = new TextDecoder();
const elements: RouteMatrixElement[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
// チャンクをデコード
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n').filter((line) => line.trim());
for (const line of lines) {
try {
const element: RouteMatrixElement = JSON.parse(line);
// originIndex と destinationIndex が存在する要素のみ追加
if (
element.originIndex !== undefined &&
element.destinationIndex !== undefined
) {
elements.push(element);
}
} catch (e) {
// JSON パースエラーは無視(空行など)
continue;
}
}
}
return elements;
} catch (error: any) {
throw new Error(`距離行列の取得に失敗しました: ${error.message}`);
}
}
距離行列の構築
/**
* ストリーミングで取得した RouteMatrixElement を距離行列に変換
*/
function buildDistanceMatrix(
elements: RouteMatrixElement[],
originsCount: number,
destinationsCount: number
): (DistanceMatrixElement | null)[][] {
// N×M の行列を初期化(すべて null)
const matrix: (DistanceMatrixElement | null)[][] = Array.from(
{ length: originsCount },
() => Array(destinationsCount).fill(null)
);
// ストリーミングで取得した要素を行列に配置
for (const element of elements) {
if (
element.originIndex === undefined ||
element.destinationIndex === undefined
) {
continue;
}
const i = element.originIndex;
const j = element.destinationIndex;
// ルートが存在する場合のみ追加
if (
element.condition === 'ROUTE_EXISTS' &&
element.distanceMeters !== undefined &&
element.duration !== undefined
) {
matrix[i][j] = {
distance: element.distanceMeters,
duration: RoutesClient.parseDuration(element.duration), // "123s" → 123
};
}
}
return matrix;
}
Distance Matrix API との比較
変更前(Distance Matrix API):
// GET リクエスト(Query param)
const response = await fetch(
`https://maps.googleapis.com/maps/api/distancematrix/json?` +
`origins=${encodeURIComponent(originsStr)}&` +
`destinations=${encodeURIComponent(destinationsStr)}&` +
`mode=${travelMode.toLowerCase()}&` +
`key=${apiKey}`
);
const data = await response.json(); // ← 同期レスポンス
const matrix = data.rows.map((row: any) =>
row.elements.map((element: any) => ({
distance: element.distance.value,
duration: element.duration.value,
}))
);
変更後(Routes API):
// POST リクエスト(JSON Body)
const request: ComputeRouteMatrixRequest = {
origins: origins.map((o) => ({
waypoint: RoutesClient.createWaypointFromLatLng(o.lat, o.lng),
})),
destinations: destinations.map((d) => ({
waypoint: RoutesClient.createWaypointFromLatLng(d.lat, d.lng),
})),
travelMode: 'DRIVE',
};
// ストリーミングレスポンスを処理
const elements = await client.computeRouteMatrix(request);
const matrix = buildDistanceMatrix(elements, origins.length, destinations.length);
フィールドマスク戦略
Routes APIでは X-Goog-FieldMask ヘッダーが必須です。必要なフィールドのみを指定することで、レスポンスサイズを削減できます。
computeRoutes のフィールドマスク
// 推奨フィールドマスク
const fieldMask =
'routes.duration,' +
'routes.distanceMeters,' +
'routes.polyline.encodedPolyline,' +
'routes.legs,' +
'routes.optimizedIntermediateWaypointIndex';
computeRouteMatrix のフィールドマスク
// 推奨フィールドマスク
const fieldMask =
'originIndex,' +
'destinationIndex,' +
'distanceMeters,' +
'duration,' +
'condition';
フィールドマスク一覧
computeRoutes:
-
routes.duration- 総所要時間 -
routes.distanceMeters- 総距離 -
routes.polyline.encodedPolyline- ルートのポリライン -
routes.legs- 区間詳細 -
routes.optimizedIntermediateWaypointIndex- 最適化後の地点順序 -
*- すべてのフィールド(デバッグ用)
computeRouteMatrix:
-
originIndex- 出発地インデックス -
destinationIndex- 目的地インデックス -
distanceMeters- 距離 -
duration- 所要時間 -
condition- ルート存在確認(ROUTE_EXISTS/ROUTE_NOT_FOUND) -
*- すべてのフィールド(デバッグ用)
エラーハンドリング
Routes APIのエラーレスポンス例:
{
"error": {
"code": 400,
"message": "Invalid request",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "origin",
"description": "origin is required"
}
]
}
]
}
}
共通エラーコード
| エラーコード | 説明 | 対処 |
|---|---|---|
| 400 INVALID_REQUEST | リクエストが無効 | リクエストパラメータを確認 |
| 403 PERMISSION_DENIED | APIキーの権限不足 | API有効化、キー制限を確認 |
| 429 RESOURCE_EXHAUSTED | レート制限超過 | リトライロジック実装 |
| 503 UNAVAILABLE | サービス一時停止 | 指数バックオフでリトライ |
エラーハンドリング実装例
try {
const response = await client.computeRoutes(request);
return response;
} catch (error: any) {
if (error.message.includes('PERMISSION_DENIED')) {
throw new Error('Google Maps APIキーの設定を確認してください');
}
if (error.message.includes('RESOURCE_EXHAUSTED')) {
throw new Error('APIリクエスト上限に達しました。しばらく待ってから再試行してください');
}
throw new Error(`ルート計算に失敗しました: ${error.message}`);
}
よくあるエラーと対策
1. 400 INVALID_REQUEST - フィールドマスクが未指定
エラーメッセージ:
Field mask is required
原因: X-Goog-FieldMask ヘッダーが未指定、または空文字列
対策:
// ❌ NG
headers: {
'X-Goog-Api-Key': apiKey,
// X-Goog-FieldMask が未指定
}
// ✅ OK
headers: {
'X-Goog-Api-Key': apiKey,
'X-Goog-FieldMask': 'routes.duration,routes.distanceMeters', // ← 必須
}
2. 403 PERMISSION_DENIED - API有効化エラー
エラーメッセージ:
Routes API has not been used in project XXXXX before or it is disabled
原因: Google Cloud Console で Routes API が有効化されていない
対策:
- Google Cloud Console にアクセス
- 「APIとサービス」→「ライブラリ」
- 「Routes API」を検索して有効化
- 5-10分待ってから再試行
3. computeRouteMatrix でデータが取得できない
症状: elements 配列が空、またはデータが欠損
原因: ストリーミングレスポンスの処理ミス(response.json() を使用している)
対策:
// ❌ NG - 同期JSONとして扱っている
const data = await response.json(); // ← ストリーミングでは動作しない
// ✅ OK - ReadableStream として処理
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// チャンクをパース...
}
4. 429 RESOURCE_EXHAUSTED - レート制限超過
エラーメッセージ:
Quota exceeded for quota metric 'Elements per day'
原因: 1日あたりの API 使用量上限に到達
対策:
- キャッシュを実装してAPI呼び出し回数を削減
- Google Cloud Console で割当量を確認・増加申請
- フィールドマスクで必要最小限のフィールドのみ取得
キャッシュについて
Routes APIのレスポンスをキャッシュすることで、API使用量を削減できます。
基本的な実装パターン
export async function getDistanceMatrix(
origins: Array<{ lat: number; lng: number }>,
destinations: Array<{ lat: number; lng: number }>,
travelMode: 'DRIVING' | 'WALKING' | 'BICYCLING' = 'DRIVING'
): Promise<DistanceMatrixResult> {
// キャッシュキー生成(座標 + 移動手段でユニークに)
const cacheKey = `distance-matrix:${JSON.stringify({origins, destinations, travelMode})}`;
// キャッシュチェック
const cached = getCache(cacheKey);
if (cached) {
return cached;
}
// API呼び出し
const result = await fetchDistanceMatrix(origins, destinations, travelMode);
// キャッシュに保存
setCache(cacheKey, result, TTL);
return result;
}
まとめ
この記事では、Google Maps Routes APIへの移行方法を実例とともに解説しました:
- ✅ Directions API → Routes API (
computeRoutes) の移行- GET → POST + JSON Body形式に変更
-
optimizeWaypointOrder: trueでルート最適化 - Header認証(
X-Goog-Api-Key)
- ✅ Distance Matrix API → Routes API (
computeRouteMatrix) の移行- ストリーミングレスポンスの処理(
ReadableStream) - 距離行列の構築
- ストリーミングレスポンスの処理(
- ✅ Routes API共通クライアントの設計
- 共通ヘッダー(
X-Goog-Api-Key,X-Goog-FieldMask) - エラーハンドリング
- 共通ヘッダー(
- ✅ フィールドマスク
- 必須ヘッダー
X-Goog-FieldMask - 必要なフィールドのみを指定
- 必須ヘッダー
- ✅ キャッシュの考え方
- API使用量の削減
次のステップ
- Routes API 公式ドキュメントで詳細を確認
- computeRoutes リファレンスで仕様を確認
- computeRouteMatrix リファレンスで仕様を確認
参考資料
Delivroute について
Delivrouteは、配送ルート最適化を簡単に行えるWebアプリケーションです。
- 🗺️ 最大10地点のルート最適化: Google Maps Routes APIで最適な配送順序を自動計算
- ⚡ 簡単CSVインポート: CSVファイルから一括で配送先を登録
- 💰 クレジット制: 無料プラン(月3回)、Proプラン(月60回)で気軽に利用可能
👉 サービスはこちら: https://delivroute.com
この記事が役に立ったら、ぜひいいね👍やコメント💬をお願いします!
Discussion