Build a light Google places API micro service for your projects.

Photo by Jackson So on Unsplash
  • Back-end: NestJS / MySql
  • Front-end: VueJS / Quasar

The Client Side Workflow

I’ll present my first approach (without quasar component, I’ll explain why later):

<div class="custom__field__control">
<input
v-model="user.adresse"
type="text"
ref="locations"
class="custom__input"
placeholder="Adresse"
/>
</div>
window.checkAndAttachMapScript = function (callback) {
let scriptId = 'map-api-script'
let mapAlreadyAttached = !!document.getElementById(scriptId)
if (mapAlreadyAttached) {
if (window.google)
callback()
} else {
window.mapApiInitialized = callback
let script = document.createElement('script')
script.id = scriptId
script.src = `https://maps.googleapis.com/maps/api/js? key=${process.env.GOOGLE_API}&sessiontoken=${this.sessionToken}&libraries=places,geometry&callback=mapApiInitialized`
document.body.appendChild(script)
}
return mapAlreadyAttached
}
data() {
return {
sessionToken: Date.now()
}
}
initLocationSearch() {
let autocomplete = new window.google.maps.places.Autocomplete(this.$refs.locations)
autocomplete.addListener('place_changed', () => {
let place = autocomplete.getPlace()
if (place && place.address_components) {
place.address_components.forEach((property) => {
if (property.types.includes('street_number')) {
this.adresse = property.long_name
this.street_number = property.long_name
}
if (property.types.includes('route')) {
this.adresse += ` ${property.long_name}`
this.route = property.long_name
}
if (property.types.includes('locality')) {
this.ville = property.long_name
}
if (property.types.includes('postal_code')) {
this.codePostal = property.long_name
}
})
}
if (place && place.geometry) {
this.geometry = place.geometry.location
}
})
},
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch)
}
Results of google places api call
Photo by David Pupaza on Unsplash
<q-input 
ref="locations"
v-model="user.adresse"
type="text"
label="Adresse"
filled
dense
/>
Console error, not an instance of HTMLInputElement
Places autocomplete, server-side and client-side

The Server Side Workflow

So let’s create a google ressource with the nest command:

nest g resource google
//GoogleInterfaces.ts\\import { Request } from "express";export interface IPlaceDetailsRequest extends Request {
query: {
place_id: string;
};
}
interface IPredictionSubstring {
length: number;
offset: number;
}
interface IStructuredFormatting {
main_text: string;
main_text_matched_substrings: Array<IPredictionSubstring>;
secondary_text: string;
}
interface IPredictionTerm {
offset: number;
value: string;
}
interface IAutocompletePrediction {
description: string;
matched_substrings: Array<IPredictionSubstring>;
place_id: string;
structured_formatting: IStructuredFormatting;
terms: Array<IPredictionTerm>;
types: Array<string>;
distance_meters?: number;
}
export interface IAutocompletePredictionContainer {
predictions: Array<IAutocompletePrediction>;
}
interface AddressComponents {
long_name: string;
short_name: string;
types: Array<string>;
}
interface Place {
address_components: Array<AddressComponents>;
}
export interface IPlacesDetailsResponse {
html_attributions: Array<string>;
result: Place;
status: string;
info_messages?: Array<string>;
}
import { Controller, Get } from "@nestjs/common";
import { GoogleService } from "./google.service";
import { Observable } from "rxjs";
import { Req, Query } from "@nestjs/common";
import {IAutocompletePredictionContainer,IPlaceDetailsRequest,} from "../interfaces/GoogleInterfaces";@Controller("google")export class GoogleController {
constructor(private readonly googleService: GoogleService) {}
@Get("places")
async getAdress(
@Query('input') searchString: string,
): Promise<Observable<IAutocompletePredictionContainer>> {
const result = this.googleService.getAdress(searchString);
return result;
}
@Get("place/details")
async getAdressDetails(
@Req() req: IPlaceDetailsRequest,
): Promise<Observable<IPlaceDetailsRequest>> {
const result = this.googleService.getAdressDetails(req.query);
return result;
}
}
import { Injectable } from "@nestjs/common";
import { map } from "rxjs/operators";
import { HttpService } from "@nestjs/axios";
import { IAutocompletePredictionContainer, IPlaceDetailsRequest }from "../interfaces/GoogleInterfaces";
import { Observable } from "rxjs";
@Injectable()
export class GoogleService {
constructor(private httpService: HttpService) {}
getAdress(input: string): Observable<IAutocompletePredictionContainer> {
return this.httpService.get("https://maps.googleapis.com/maps/api/place/autocomplete/json", {
params: {
input,
key: process.env.GOOGLE_API,
sessionToken: Date.now(),
types: "geocode",
components: "country:fr",
},
}).pipe(map((res) => res.data.predictions));
}
getAdressDetails(input: {
place_id: string;
}): Observable<IPlaceDetailsRequest> {
const { place_id } = input;
const sessionToken = Date.now();
return this.httpService.get(
`https://maps.googleapis.com/maps/api/place/details/json?place_id=${place_id}&key=${process.env.GOOGLE_API}&sessionToken=${sessionToken}`,
).pipe(map((res) => res.data));
}
}
<div class="col-6" v-if="editable">
<q-select
class="q-ma-md"
@input-value="fetchPlaces($event)"
v-model="user.adresse"
autocomplete="places"
label="Adresse"
use-input
:options="placesDescription"
map-options
emit-value
></q-select>
</div>
async fetchPlaces(searchString) {
clearTimeout(this.addressSearchTimeout);
this.addressSearchTimeout = setTimeout(async () => {
this.places = (await googleServices.getPlaces(searchString)).data;
}, 500);
},
watch: {
places: {
deep: true,
handler(place) {
if (place?.length) {
this.placesDescription = place.map((location) => {
return {
label: location.description,
value: location.description,
place_id: location.place_id,
};
});
}
},
},
user: {
deep: true,
async handler(location) {
const placeToCall = this.placesDescription.filter(
(place) => place.label === location.adresse
);
googleServices.checkIfPropertyExist(this.user, placeToCall);
},
},
}
// /services/google.js \\import { axiosInstance } from "src/boot/axios";export default {
getPlaces(input) {
return axiosInstance.get("/google/places", { params: { input } });
},
getPlacesDetails(data) {
return axiosInstance.get("/google/place/details", { params: data });
},
async checkIfPropertyExist(entity, data) {
if (data.length) {
const result = await this.getPlacesDetails({
place_id: data[0].place_id,
});
if (result.status === 200) {
result?.data?.result?.address_components.forEach((detail) => {
if (detail?.types.includes("postal_code")) {
entity.codepostal = detail.long_name;
}
if (detail?.types.includes("locality")) {
entity.ville = detail.long_name;
}
});
if (result?.data?.result?.geometry?.location) {
entity.lat = result?.data?.result?.geometry?.location.lat;
entity.lng = result?.data?.result?.geometry?.location.lng;
}
}
}
},
};
Q-Select quasar component with Google places api results
Quasar Q-select with Google Places api results

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store