Build a light Google places API micro service for your projects.
…or how to switch your api calls into your back-end instead of your front-end…
Recently I had to manage an autocomplete input which will call Google places API to suggest adresses for user profil pages.
Stack:
- 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):
First a basic input with a ref (important):
<div class="custom__field__control">
<input
v-model="user.adresse"
type="text"
ref="locations"
class="custom__input"
placeholder="Adresse"
/>
</div>
Create a function which accept a callback to inject places api call into the DOM.
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
}
Call Explanation:
process.env.GOOGLE_API: your google api key
geometry: To add latitude and longitude to our response
sessionToken: To avoid multiple request on google api, and don’t spend a lot of money while test your features, we created a token variable with a random number:
data() {
return {
sessionToken: Date.now()
}
}
Then we create a function that’ll attach our api call to our input, and manage results:
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
}
})
},
The first part here is to attach the script to our “locations” ref.
The second part is to treat result and assign our variables with the corresponding google places autocomplete element.
For the last part, we’ll have to start this script at the beginning of the component lifecycle:
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch)
}
Result:

Ok that’s not bad, I manage the call with a classic input, now I’ll try to explain my attempt with quasar component…
I will not explain what’s quasar, and how their components works, but my idea was to create a ref from a quasar input to inject google results on it.
Here is my quasar component:
<q-input
ref="locations"
v-model="user.adresse"
type="text"
label="Adresse"
filled
dense
/>
And if you try with the same config as above, for classic html input..it doesn’t work !
And your console look like this:

The thing is that quasar will wrap the classic input into the q-input component, so after reading some threads from the community, trying to retrieve the ref inside the quasar component but without any success, some people from the community tell me that using the ref for this purpose is not the right way to solve this problem.
“you have to assign your api result inside a variable and use a Q-select to display the results”…
And going back to the google places api doc to grab some informations, we can see this on the top of the page:

That’s interesting to know that we can do all the stuff into our back-end (Server side) and send the result from our back-end to our front-end and give and give it to our Q-select component
The Server Side Workflow
So let’s create a google ressource with the nest command:
nest g resource google
Create needed interfaces for your responses:
//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>;
}
If you want to see those interfaces more in details…
Now let’s create our controller:
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;
}
}
And our service:
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));
}
}
Here we have two endpoint, the first one get our searchstring and will return us some suggestions and in our case the locations place_id with which we’ll give, with the second request all our places details… (lat,lng and many more…)
Let’s see how to manage our responses in our front-end:
Like the community said to me, we’ll use a q-select component to load the result of our first request and when we select an address, send an other request to our ‘details’ endpoint:
The q-select component:
<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>
The @input-value here will send our request with what we’ll type into our component with this methods, where we use a setTimeout to avoid send a call for each letter:
async fetchPlaces(searchString) {
clearTimeout(this.addressSearchTimeout);
this.addressSearchTimeout = setTimeout(async () => {
this.places = (await googleServices.getPlaces(searchString)).data;
}, 500);
},
We’ll use two watcher, the first one is to “watch” if this.places get some changes, if yes we gonna change “this.placesDescription” thats is our options for our q-select component (you’ll see the result of our first endpoint into your component):
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);
},
},
}
And the second one will watch this.user to call our second endpoint with the place_id from the first endpoint, call the second to get more details about the place and update our user address property with it:
// /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;
}
}
}
},
};
And here you are!
Instead of make a call from your front end, you create to endpoint to manage places api call and use the data to display it in your front-end app, and we can’t see the “powered by google” anymore !!

You can now make your back-end online, and for all of your project you can call those endpoint when you want to fill some address property into user’s profil, or anything you want to build with places api!
Hope it helps, and if you have any suggestions feel free to contact me !!
I didn’t explain why I used rxjs with pipe, map, and observable cause I don’t know (yet) those tools but the first call of google places api will return an observable, and after a few time reading the doc it’s the only way that I found to use the data from api….
Thanks for reading :)