Create Your Own Geocode Serverless Application - on the JAMStack

By Raymond Camden | 09 March 2020

Try HERE Maps

Create a free API key to build location-aware apps and services.

Get Started

As developers in our industry know, we love coming up with new names for things. The "JAMStack" is the latest in this trend. JAMStack refers to static web sites that make use of JavaScript and serverless technology to be dynamic. So a static site... that isn't. JAM stands for JavaScript, APIs, and Markup. A typical JAMStack site will use JavaScript on the client to enable interactivity and use APIs to interact with services providing data and other features. Markup typically refers to markdown. Most JAMStack sites will use a static site generator that will take markdown files and output HTML. That's not required though - you can write simple HTML or generate a single page application with a front-end framework like Vue.js. Let's look at an example of using HERE APIs on a simple JAMStack site.

Let's start off by taking a look at the result. I built a simple one page application that lets you input a starting and ending location. Once you do that, the application will give you a route between both points. For a little extra information, it also reports on the weather in both locations. It may be stormy where you are now but beautiful where you're ending up. I used the following aspects in my demo:

  • HTML - ok, that's obvious - but I point this out to say that due to the small size of the demo, I didn't bother with a static site generator.
  • Vue.js - To handle the interactivity of the demo I used a bit of Vue.js. I didn't create a SPA, it's just  a simple script tag, but Vue works great for simple progressive enhancement of HTML pages.
  • Serverless functions - Instead of directly accessing HERE APIs via JavaScript in the browser, I set up two serverless end points that proxy the calls to HERE. This lets me hide my API key and massage that data a bit as well.
  • Finally, I deployed my site on Netlify. There's multiple places you can host a JAMStack site, but Netlify is my favorite. When I'm not using Netlify, I also use Zeit
  • You can read more about the JAMStack at https://jamstack.org.

You can play with my demo here: https://netlifyhere.netlify.com/. The source code everything I'm sharing my be found here: https://github.com/cfjedimaster/netlifyhere.

Alright, with that long preamble out of the way, let's talk about the app!

First, I'll discuss the HTML and JavaScript of the front end. The HTML portion handles loading my CSS, JavaScript, and handles the dynamic layout that Vue's going to enhance and make awesome.


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Netlify HERE</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>

	<div id="app">
		<h2>Netlify HERE Demo</h2>
		<p>
			This app demonstrates using HERE services via serverless functions hosted on a static site. Enter a starting
			destination and final destination in the fields below. The app will then find the weather at both locations
			as well as determine a route between them.
		</p>

		<form>
			<p>
			<label for="start">Starting Location:</label>
			<input type="text" v-model="start" id="start">
			</p>

			<p>
			<label for="end">End Location:</label>
			<input type="text" v-model="end" id="end">
			</p>
				
			<p>
				<button @click.prevent="generateData">Get Weather and Route</button>
			</p>
		</form>

		<div v-if="loading">
			<p><i>I'm gathering your data now, please stand by!</i></p>
		</div><div v-else-if="hasResult">
			<h2>Results</h2>
			<p>
				The weather at your starting location is <b>{{ weatherStart.description }}</b> 
				with a low of {{ weatherStart.lowTemperature }} and a high of {{ weatherStart.highTemperature }}.
			</p>
			<p>
				The weather at your destination is <b>{{ weatherEnd.description }}</b> 
				with a low of {{ weatherEnd.lowTemperature }} and a high of {{ weatherEnd.highTemperature }}.
			</p>
			<p>

				Your route will take {{ summary.duration | formatSeconds }} and cover {{ summary.length | formatDistance }}:

				<ul>
					<li v-for="step in steps">{{step.instruction}}</li>
				</ul>
			</p>
		</div>

	</div>
	
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
	<script src="app.js"></script>
</body>
</html>

The first part of the layout handles prompting for the starting and end location. The next part handles displaying the results. I print out a weather report for both locations and then return the steps of the route between them (along with a quick summary). For folks new to Vue, the tokens you see in the HTML end up being replaced by real data. The v-if and v-for directives get executed as you imagine they would. Now let's look at the JavaScript in the Vue application.


// Summarize to hours or seconds, it's rough but ok
Vue.filter('formatSeconds', s => {
	let hours = Math.floor(s/(60*60));
	if(hours > 2) return hours + ' hours';
	let minutes = Math.floor(s/60);
	return minutes + ' minutes';
});

// Always meters, switch to miles
Vue.filter('formatDistance', s => {
	return Math.floor(s/1609) + ' miles';
});


const app = new Vue({
	el:'#app',
	data: {
		start:'Lafayette, LA',
		end: 'Chicago, IL',
		weatherStart:'',
		weatherEnd:'',
		steps:[],
		summary:null,
		loading:false,
		hasResult:false
	},
	methods: {
		async generateData() {
			this.hasResult = false;
			this.loading = true;
			console.log('running generateData, values are '+this.start+' and '+this.end);
			this.weatherStart = await getWeather(this.start);
			this.weatherEnd = await getWeather(this.end);
			let route = await getRoute(this.start, this.end);
			this.steps = route.actions;
			this.summary = route.summary;
			this.loading = false;
			this.hasResult = true;
		}
	}
});

async function getRoute(x,y) {
	let resp = await fetch(`/.netlify/functions/getRoute?start=${x}&end=${y}`);
	let data = await resp.json();
	return data;
}

async function getWeather(location) {
	let resp = await fetch(`/.netlify/functions/getWeather?location=${location}`);
	let data = await resp.json();
	return data;
}

My application begins with two formatting functions that help make data in the front end a bit more readable. After that, the real meat of the application begins. I set up my data (with some defaults to save me from having to type while I tested) and then define the function that's called when the user hits the submit button back in the HTML. generateData fires off HTTP calls to my two serverless end points. First to get the weather and then to get the route. Once done it saves those values. And that's it. Basically my code is just handling formatting and user interaction. The real stuff is over on the serverless side.

Netlify added it's serverless support back at the beginning of 2019. It uses Amazon Lambda behind the scenes but hides the complexity of Lambda behind a much easier to use system. Developers can write simple JavaScript (or Go) functions and Netlify handles creating a URL for them. A simple Netlify function may look like this:


exports.handler = async (event, context) => {
  const name = event.queryStringParameters.name || "World";

  return {
    statusCode: 200,
    body: `Hello, ${name}`
  };
};

Given that this is named "helloWorld.js", Netlify would make it accessible on your site as yourdomain.com/.netlify/functions/helloWorld. Again, this is just a front-end to Amazon Lambda, but it's significantly simpler to use than Amazon. Plus, you can include your serverless functions along with the rest of your site in the same repository.

Another useful aspect of Netlify Functions is that they allow you to define environment variables and then make use of them in your code. This is how I can share my code on a public GitHub repository - I simply address the environment variable for my keys instead of directly accessing them. 

Let's take a look at the first serverless function, getWeather.

 


/* eslint-disable */
const fetch = require('node-fetch');

const HERE_APP_CODE = process.env.HERE_APP_CODE;
const HERE_APP_ID = process.env.HERE_APP_ID;

exports.handler = async function(event, context, cb) {

  let location = event.queryStringParameters.location;
  if(!location) {
    return {
      statusCode:500,
      body:'Must define location'
    }
  }

  let url = `https://weather.cc.api.here.com/weather/1.0/report.json?app_code=${HERE_APP_CODE}&app_id=${HERE_APP_ID}&product=observation&name=${encodeURIComponent(location)}&metric=no`;

  let response = await fetch(url);
  let data = await response.json();

  // filter it down a bit
  let result = data.observations.location[0].observation[0];
  // high and low temps are to the hundreds, no one cares about that
  result.lowTemperature = Math.floor(result.lowTemperature);
  result.highTemperature = Math.floor(result.highTemperature);
  
  return {
    headers: {
      'Content-Type':'application/json'
    },
    statusCode: 200,
    body: JSON.stringify(result)
  };

}

The function begins by checking to ensure that a location query parameter was sent, if not, an immediate error is returned. Next it creates a URL that hits the HERE Weather API. Once the API is hit, I then manipulate the results a bit to make it simpler for the front end. I also notice that temperatures were reported to the hundreds place, so for example, not just 78 degrees but 78.02. Since most humans can't tell the difference I just simplified those values. Here's an example result of calling this function.

 


{
	"daylight": "D",
	"description": "Light rain. Mostly cloudy. Mild.",
	"skyInfo": "16",
	"skyDescription": "Mostly cloudy",
	"temperature": "75.00",
	"temperatureDesc": "Mild",
	"comfort": "74.97",
	"highTemperature": 74,
	"lowTemperature": 60,
	"humidity": "85",
	"dewPoint": "70.00",
	"precipitation1H": "0.00",
	"precipitation3H": "*",
	"precipitation6H": "*",
	"precipitation12H": "*",
	"precipitation24H": "*",
	"precipitationDesc": "Light rain",
	"airInfo": "*",
	"airDescription": "",
	"windSpeed": "12.64",
	"windDirection": "180",
	"windDesc": "South",
	"windDescShort": "S",
	"barometerPressure": "29.77",
	"barometerTrend": "",
	"visibility": "7.00",
	"snowCover": "*",
	"icon": "18",
	"iconName": "sprinkles",
	"iconLink": "https://weather.cc.api.here.com/static/weather/icon/27.png",
	"ageMinutes": "69",
	"activeAlerts": "26",
	"country": "United States",
	"state": "Louisiana",
	"city": "Lafayette",
	"latitude": 30.2241,
	"longitude": -92.0198,
	"distance": 2.32,
	"elevation": 11,
	"utcTime": "2020-03-04T12:53:00.000-06:00"
}

You'll notice there's a lot of information here I don't use. I could further optimize my application by having the serverless function only return exactly what my Vue code is using. That would make things a bit quicker for the user as well.

Now let's take at the routing aspect. This one's a bit more complex because we have to use two APIs. The Routing API requires a precise location in longitude and latitude. We can't expect our users to know those values. So to make it work for them entering text-based locations we make use of the Geocoding API as well. Let's take a look at that function.


/* eslint-disable */
const fetch = require('node-fetch')

const HERE_API_KEY = process.env.HERE_API_KEY;


exports.handler = async function(event, context) {

  let location1 = event.queryStringParameters.start;
  let location2 = event.queryStringParameters.end;
  if(!location1 || !location2) {
    return {
      statusCode:500,
      body: 'Must pass start and end parameters.'
    }
  }

  let location1Position = await getLocation(location1);
  let location2Position = await getLocation(location2);
  let route = await getRoute(location1Position.lat+','+location1Position.lng, location2Position.lat+','+location2Position.lng );

  let data = route.sections[0];
  delete data.polyline;

  return {
    headers: {
      'Content-Type':'application/json'
    },
    statusCode: 200,
    body: JSON.stringify(data)
  }
}

async function getLocation(x) {
   let url = `https://geocode.search.hereapi.com/v1/geocode?q=${encodeURIComponent(x)}&apikey=${HERE_API_KEY}`;
   let response = await fetch(url);
   let data = await response.json();
   // assume first result
   return data.items[0].position;
}

async function getRoute(x, y) {
  let url = `https://router.hereapi.com/v8/routes?transportMode=car&origin=${x}&destination=${y}&units=imperial&return=summary,actions,instructions,polyline&apikey=${HERE_API_KEY}`;
console.log(url);
  let response = await fetch(url);
  let data = await response.json();
  return data.routes[0];

}

As with the previous function, I begin with some simple validation. This one takes two arguments representing the beginning and end of the route. I first geocode the locations to translate the free form inputs into a longitude and latitude. Then I simply use the routing API using the new values. As before, I do a bit of manipulation on the result to make it simpler for the front end code to use.

And that's it! As a reminder, you can test this out yourself here and see the code on my repository.