Skip to content

Instantly share code, notes, and snippets.

@hamishcampbell
Created August 7, 2015 23:03
Show Gist options
  • Save hamishcampbell/2c26010c4d591aacba54 to your computer and use it in GitHub Desktop.
Save hamishcampbell/2c26010c4d591aacba54 to your computer and use it in GitHub Desktop.
SoT Hackfest API Suff

SoT Hackfest Auckland 2015

Welcome! This is a brief, opinionated guide to getting started building a browser-based web app with RESTful APIs. The intention is to provide a starting point for building stuff for SoT Hackfest 2015, and provides just one approach. It is not the only approach, nor is it necessarily the best approach for a production system, but it's a good way to jumpstart your hack.

Useful references

This guide will use the very popular jQuery javascript abstraction library. jQuery has excellent API documentation here:

You can load it from the jQuery CDN (easy) or download it to include in your project (reliable).

If you haven't seen it already, Alex Gibsons list of data sources is invaluable:

I work for Koordinates, and we provide the APIs behind a number of key data sites, notably:

We have API documentation for using these APIs here:

There are lots of other APIs you could use (see Alex's list), but for the purposes of this guide, I'll be using data from those sites.

Jumping right in...

Let's try to answer the question: "What is the elevation at your current location."

We'll use the LINZ Data Service API to query the following data layer:

(You can click on the "Services" tab to see the available API services for the layer).

This data layer is basically a big single band image (lot's of images, really) where the pixel value of a cell is the elevation, in metres at that location.

To get the user's location we'll going to use the HTML5 Geolocation Services API:

We'll also make use of jQeury $.get and $.Deferred to handle the asyncronous API calls.

Code

<!DOCTYPE html>
<html lang="en">
	<meta charset="utf-8">	
	<style>
	* { font-family: helvetica, sans-serif; }
	ol li:last-child { color: green; }
	</style>
	<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
	<script>
	$(document).ready(function() {

		// Your LINZ Data Service API Key
		var ldsApiKey = "[GET AN API KEY, PUT IT HERE]";

		// The Layer ID of the layer we're querying
		var ldsLayerID = 1768;

		/**
		 * Adds a message to the `messageNode`
		 *
		 * @param text  The text, safely cleaned below.
		 */
		var logMessage = function(text) {
			var newNode = $('<li>');
			newNode.text(text);
			$('#messages').append(newNode);
		}

		/**
		 * Given a successful data query, returns the value for the layer,
		 * or null if the point was invalid.
		 *
		 * @param data  The object returned by the LDS API
		 * @return float|null
		 */
		var getElevationFromResponse = function(data) {
			var layerData = data.rasterQuery.layers[ldsLayerID];
			if (layerData.status === "ok") {
				return layerData.bands[0].value;
			}
			return null;
		}

		/**
		 * Initiates a GET request to the LINZ Data Service for the elevation layer,
		 * and returns a Deferred that will be resolved when the elevation is returned,
		 * or rejected if there was an error.
		 *
		 * @param lat   The latitude (y) of the position (WSG84 Coordinates)
		 * @param lon   The longitude (x) of the position (WSG84 Coordinates)
		 * @return Deferred
		 */
		var requestElevation = function(lat, lon) {
			// Create the Deferred object.
			var deferred = $.Deferred();

			// These are our query string parameters for the API call.
			var params = {
				"key": ldsApiKey,
				"layer": ldsLayerID,
				"x": lon,
				"y": lat
			}

			// Initiate the request *asyncronously*
			var request = $.get("http://api.data.linz.govt.nz/api/rasterQuery.json", params);

			// Add a success handler, a function to be called when
			// the API returns a successful result.
			request.done(
				function(data) {
					// When we good data, we extract the elevation
					// and "resolve" the deferred object with the result.
					var elevation = getElevationFromResponse(data);
					deferred.resolve(elevation);
				}
			);

			// Add a failure handler, a function to be called when
			// the API rejects the request.
			request.fail(
				function() {
					// When the request failes, we reject the deferred object.
					deferred.reject()
				}
			);

			// Return the deferred. It will "resolve" or "reject" when the a
			// result is available.
			return deferred;
		}

		logMessage("Getting current position...");
 		navigator.geolocation.getCurrentPosition(function(position) {
 			logMessage("Got current position, querying data...");

 			// Request the elevation. The function returns a deferred object --
 			// REMEMBER: we won't know the result of the call until it has finished querying the
 			// remote service and the deferred is resolved.
 			var deferred = requestElevation(position.coords.latitude, position.coords.longitude);

 			// If we get a good result, print it:
 			deferred.done(function(elevation) {
 				logMessage("The elevation here is: " + elevation + "m!");
 			});
 			// Otherwise, *sadface*
 			deferred.fail(function() {
 				logMessage("Couldn't get the elevation here :(");
 			});
		});

	});
	</script>
	<body>
		<h1>Find your elevation at this location</h1>
		<p>Note that geolocation services must be enabled.</p>
		<ol id="messages"></ol>
	</body>
</html>

Ok, what's going on here.

  1. We asked the browser to give us the users location as a set of coordinates.
  2. We sent those coordinates to the LINZ Data Service API with an HTTP GET request.
  3. When we got a response back we extracted the data we wanted and displayed it.

This uses an asyncronous (commonly called "AJAX") pattern where the result of function calls are "deferred" objects that are provide handles to attach events when other things occur (like an HTTP response, which could take a while to respond).

Ok, tell me more about AJAX

That $.get method bundles up lots of compexity. You can roll this yourself but I wouldn't recommend it. The $.get method:

  1. Builds the URL to call, correctly encoding the parameters you supply into a query string and appending it to the URL cleanly.
  2. Initiates a GET request using whatever capabilities the browser provides,
  3. Negotiates content type, then automatically and safely parses the response into a Javascript object,
  4. Checks the HTTP Status Code and resolve/rejecting the result as appropriate, with useful information.

What's up with that dollar ($) sign?

$ is a shortcut for jQuery. It's a function, but also an object that contains a bunch of handy functions and helper objects. Questionable design choices, but it seems to work.

How is it used above:

  • $(document).ready(function() { }); -- this is a special form that ensures that whatever is contained in the function body is executed when the document is ready. For a number of reasons, you generally want to wrap all of your javascript in that.
  • $('<li>') -- this creates a new li element. In fact, put any HTML tag there and it will create a DOM element (wrapped in jQuery extras) which you can then place in the document.
  • $('#messages') -- this is the 'selector' sytnax - this finds any tags that match the string and wraps it in a node list thing, that you can then manipulate. E.g. this is an ID selector that finds the element with id=messages. This: $('div').text('HI') will set the inner text of all div elements on the page to Hi.
  • $('#messages').append(newNode) -- appends the node newNode to the messages element.
  • $.get -- is a function, described above.
  • $.Deferred -- is an object constructor, get a new Deferred by calling $.Deferred().

What about the transfer encoding?

The "X" in "AJAX" stands for XML, but XML is usually horrible to use and these days, a lot of APIs will instead JSON. JSON stands for "JavaScript Object Notation" - a very popular, lightweight and easy to use string serialization format. Our $.get method is magically processing it for us, but if you want to serialize and deserialize it properly, you should use the built in methods for the language you're using. In modern Javascript:

// Encode with JSON.stringify:
var stringEncoded = JSON.stringify({"object key": ["array", "values"]})
// returns a string: {"object key":["array","values"]}

// Decode with JSON.parse.
var theObject = JSON.parse('{"object key":["array","values"]}')
// returns the actual object

Most languages will have a 'right way' to parse and decode objects/dictionaries/hashmaps/etc to JSON.

E.g. in Python:

>>> import json
>>> json.loads("[1,2,3]")
    [1, 2, 3]
>>> json.dumps({ "foo": "bar" })
	'{"foo": "bar"}'

What about the other 'verbs' - PUT / POST / DELETE

In RESTful service design, you use other HTTP verbs to handle common actions like 'edit', 'create' and 'delete' a particular resource. You probably don't need to worry about that stuff for the Hackfest.

One exception that you might encounter is a POST request. GET requests can only send data to the API services in the URL query string, and that might not be appropriate or feasible for some types of request. In these cases you might use a POST request (e.g. with the $.post jQuery method) which provides a different way to send data via the request body.

While there are lots of technical differences in how this happens, if you use the $.post method provided by jQuery, your code should remain very similar to the example above.

Help, it's not working!

Ok, you're stuck. What to do!?

Use the console

Your browser has a dev console. Open it (find it in the menu... somewhere) and you can do all sorts of things:

  • Use console.log(variable1, ...) you can prints things into the console to see what's happening.
  • Add debugger to your code and the Javascript will break execution at that point so you can poke around
  • You can execute arbitrary javascript in the console too, useful for trying things out.
  • See the 'network' tab in the dev console to see HTTP requests timeline, with request and response body.

Read the documentation

No, really.

What responses are you getting from the API?

Different status codes in API responses can mean different things:

  • 200 is "OK", everything should be fine
  • 30x means it's moved. You need to follow redirect as supplied in the Location header. Check you've used the right URLs (maybe you're missing a trailing slash)
  • 400 means you did something wrong, for example, provided an invalid parameter value. Check the response body, maybe you got a reason back too.
  • 401 and 403 are permission failures. If 401 you might need to provide authentication details. If 403 well, you don't permission regardless.
  • 404 it's an invalid URL, not found!
  • 405 and above, it's invalid for various reasons
  • 50x means something went wrong on the service you're calling and there isn't much you can do. Try again later, hopefully they'll fix it.

Ask questions

But before you do, answer these two simple questions, otherwise you're wasting the other person's time:

  1. What did you expect to happen?
  2. What actually happenned?

Figure out where those two things diverge and you'll save a lot of time.

No one around? Try talking to a rubber duck. It really works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment