Most Google Maps integrations begin with one question: How do we put a pin on a map?
That is the easy part.
The harder, more valuable problem is building a system that can answer questions such as:
- Which nearby locations should a customer see?
- How long will the trip take right now?
- Which parts of a business profile changed, and does the change need attention?
- Is a local-visibility drop a data problem, a competitive problem, or simply a measurement problem?
In 2026, Google Maps Platform is moving developers toward a cleaner answer: use the map for presentation, use the newer web services for computation and place data, and keep product decisions in your own application layer.
This article walks through that architecture with TypeScript examples. The examples use a local-business use case, but the pattern applies just as well to delivery, marketplaces, store locators, field services, and travel tools.
The change that should trigger an architecture review
On February 25, 2026, Google deprecated google.maps.DirectionsService\, DirectionsRenderer\, and DistanceMatrixService\ in the Maps JavaScript API. They still work and Google says they are not currently scheduled for discontinuation, but new projects should use the recommended newer services instead. Google's release notes are clear about the direction of travel.
This is not a reason to rewrite a working map overnight. It is a reason to stop treating browser-side map services as your routing backend.
The durable split looks like this:
\text
Browser
├─ Maps JavaScript API: render map, markers, user interactions
└─ Your backend: authenticated product requests
├─ Routes API: route, ETA, route matrix
├─ Places API (New): discovery and place details
├─ Your database: snapshots, caches, audit history
└─ Your rules: alerts, prioritization, permissions
\\
That boundary matters. It keeps API keys and billing-sensitive requests off the client, gives you a single place to add caching and observability, and stops your business rules from being scattered across React components.
1. Treat the map as a view—not the source of truth
The browser should render the current product state. It should not decide which data is fresh, calculate every route directly, or store your only copy of a business location.
A useful internal model is deliberately small:
\`ts
type PlaceSnapshot = {
placeId: string;
displayName: string;
formattedAddress?: string;
location?: { lat: number; lng: number };
primaryType?: string;
businessStatus?: string;
fetchedAt: string;
};
type RouteSnapshot = {
originKey: string;
destinationKey: string;
travelMode: "DRIVE" | "WALK" | "BICYCLE" | "TRANSIT";
durationSeconds?: number;
distanceMeters?: number;
encodedPolyline?: string;
fetchedAt: string;
};
`\
fetchedAt\ is not cosmetic. Place data and travel time are observations, not eternal facts. Once you persist that fact, your UI can say “updated 12 minutes ago,” your jobs can refresh high-value data, and your audit layer can distinguish a new change from an old one.
2. Move routing behind a server endpoint
The Routes API computes routes over HTTP. Google requires a response field mask, which is a good discipline: request only the fields your feature really needs.
Here is a minimal Next.js route handler. The API key stays on the server.
\`ts
// app/api/route/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { origin, destination } = await request.json();
const response = await fetch(
"https://routes.googleapis.com/directions/v2:computeRoutes",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_SERVER_KEY!,
"X-Goog-FieldMask":
"routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline",
},
body: JSON.stringify({
origin: { location: { latLng: origin } },
destination: { location: { latLng: destination } },
travelMode: "DRIVE",
routingPreference: "TRAFFIC_AWARE",
computeAlternativeRoutes: false,
}),
},
);
if (!response.ok) {
return NextResponse.json(
{ error: "Route lookup failed" },
{ status: response.status },
);
}
return NextResponse.json(await response.json());
}
`\
The official Compute Routes guide shows the same endpoint and documents the duration\, distanceMeters\, and encoded-polyline response fields.
In production, add four things before calling this “done”:
- Input validation. Accept numeric latitude/longitude pairs or validated place IDs—never arbitrary unbounded input.
- A cache key. Include origin, destination, travel mode, and the parameters that materially affect the response.
- A short TTL for traffic-aware results. An ETA is useful precisely because it changes; avoid serving it as if it were permanent.
- Metrics. Log latency, status, cache hits, and API errors. You cannot manage an expensive dependency by looking only at browser errors.
For many-to-many dispatch or store-selection problems, use Compute Route Matrix rather than firing a large number of individual route requests. The principle is the same: do the work on the backend and persist only the result your product needs.
3. Make field masks a product contract
Places API (New) does not return a default payload when you omit a field mask; the request fails. That can feel strict at first, but it forces a useful design conversation: what information does this screen or job actually need?
For example, a location selector may only need a name, address, identifier, and coordinates:
\`ts
async function searchPlaces(textQuery: string) {
const response = await fetch(
"https://places.googleapis.com/v1/places:searchText",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": process.env.GOOGLE_MAPS_SERVER_KEY!,
"X-Goog-FieldMask":
"places.id,places.displayName,places.formattedAddress,places.location",
},
body: JSON.stringify({ textQuery, pageSize: 10 }),
},
);
if (!response.ok) throw new Error("Place search failed");
return response.json();
}
`\
Avoid using *\ outside exploratory work. Google's Text Search (New) documentation explicitly warns that wildcard masks can return unnecessary data, increasing processing and billing cost.
The bigger win is architectural: name your masks and test them.
\`ts
const PLACE_CARD_FIELDS = [
"places.id",
"places.displayName",
"places.formattedAddress",
"places.location",
].join(",");
const PLACE_AUDIT_FIELDS = [
"places.id",
"places.displayName",
"places.primaryType",
"places.businessStatus",
"places.formattedAddress",
].join(",");
`\
Now a pull request that adds a field has an obvious review question: which screen needs it, what is the expected lifecycle, and what does it cost? That is much better than a mystery payload growing in the background.
4. Model a business as a changing entity
Local products break when they represent a business as name + coordinates + rating\ forever.
A real-world location can change its hours, categories, services, address, website, phone number, or status. It can move. A service-area business may not have a customer-facing storefront at all. A new business can be relevant before its opening date.
Those are not edge cases if you are building local search, lead routing, or visibility software. They are the product.
Start with a refresh policy rather than an assumption that every field has the same freshness requirement:
\ts
const refreshPolicy = {
placeIdentity: "when user connects or edits a location",
businessStatus: "daily for monitored locations",
hours: "daily plus before showing time-sensitive UI",
competitors: "scheduled scan, not every page view",
routes: "on demand with a short traffic-aware cache",
} as const;
\\
The current Places documentation also supports discovery patterns that used to require awkward workarounds. Text Search can include pure service-area businesses with includePureServiceAreaBusinesses\, and can include future-opening businesses with includeFutureOpeningBusinesses\. Use those options only when they match the user intent; they are not a substitute for a clearly defined search experience.
One subtle but important detail: Google notes that identical Text Search requests are not guaranteed to produce a consistent list of places. That means a rank or competitor scan needs a recorded timestamp, query, viewport/bias, and response snapshot. Without those, “we dropped three places” may simply mean “we compared two different observations.”
5. Separate collection from decisions
Maps APIs can give your product excellent geographic and place data. They cannot tell an operator what to fix first.
That is your application’s job.
For a local-business workflow, the signal pipeline might look like this:
\`text
collect → normalize → compare → explain → queue an action
GBP/profile data ─┐
map-pack scans ├─→ per-location snapshot → change detector → action queue
reviews ┤
competitor data ┘
`\
A small rules layer goes a long way:
\`ts
type Finding = {
key: string;
impact: "high" | "medium" | "low";
evidence: string[];
suggestedAction: string;
};
function prioritize(findings: Finding[]) {
return findings.sort((a, b) => {
const weight = { high: 3, medium: 2, low: 1 };
return weight[b.impact] - weight[a.impact];
});
}
`\
The hard part is not the sort\. It is preserving evidence: the measured locations, the keyword, the scan time, the competing businesses, and the profile fields that informed the finding. That makes a recommendation reviewable instead of magical.
This is exactly the operating layer we built SEOG for. SEOG turns public Google Business Profile signals, geo-grid map rankings, reviews, competitor movement, and NAP consistency into a prioritized local-visibility plan—instead of leaving owners with another dashboard to decode. If you are building on Maps APIs, that distinction is useful: the API retrieves data; the product turns observations into accountable next actions.
6. A migration checklist for existing Maps JavaScript API apps
If your application still uses DirectionsService\ or DistanceMatrixService\, do not start by replacing every call mechanically. First find the product intent behind each call.
- “Show the route on the map.” Use Routes API for computation; render the returned encoded polyline in your map UI.
- “Choose the closest available technician.” Use Route Matrix, with caching and a clear freshness window.
- “Find a business from a query.” Use Places API (New) with an explicit field mask.
- “Power a local-visibility dashboard.” Store dated snapshots, define a measurement method, and compare like with like.
Then work through this implementation checklist:
- Inventory legacy routing calls and identify the user-facing behavior each one supports.
- Create server-side adapters for Routes and Places before changing the UI.
- Restrict API keys by application and API; never expose an unrestricted server key in a browser bundle.
- Define named field masks per feature and test the expected response shape.
- Store observation time, query context, and request parameters alongside results you plan to compare.
- Add caching, retries with sensible limits, error metrics, and budget alerts.
- Roll out behind a feature flag and compare new versus existing behavior before removing the legacy path.
The takeaway
The modern Google Maps stack is not “a prettier map.” It is a set of services that let you build a reliable local-data system—as long as you keep the right boundaries.
Render maps in the client. Compute routes and fetch place data through your backend. Ask for only the fields you need. Record when and how you observed local data. And build an operational layer that turns those observations into decisions people can act on.
If you want to see that last layer applied to Google Business Profile visibility, map-pack rankings, reviews, and local competitors, try the free analysis at SEOG.
Top comments (0)