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.
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.
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.
<!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>
- We asked the browser to give us the users location as a set of coordinates.
- We sent those coordinates to the LINZ Data Service API with an HTTP GET request.
- 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).
That $.get
method bundles up lots of compexity. You can
roll this yourself but I wouldn't
recommend it. The $.get
method:
- Builds the URL to call, correctly encoding the parameters you supply into a query string and appending it to the URL cleanly.
- Initiates a GET request using whatever capabilities the browser provides,
- Negotiates content type, then automatically and safely parses the response into a Javascript object,
- Checks the HTTP Status Code and resolve/rejecting the result as appropriate, with useful information.
$
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 newli
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 withid=messages
. This:$('div').text('HI')
will set the inner text of alldiv
elements on the page toHi
.$('#messages').append(newNode)
-- appends the nodenewNode
to themessages
element.$.get
-- is a function, described above.$.Deferred
-- is an object constructor, get a newDeferred
by calling$.Deferred()
.
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"}'
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.
Ok, you're stuck. What to do!?
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.
No, really.
Different status codes in API responses can mean different things:
200
is "OK", everything should be fine30x
means it's moved. You need to follow redirect as supplied in theLocation
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
and403
are permission failures. If401
you might need to provide authentication details. If403
well, you don't permission regardless.404
it's an invalid URL, not found!405
and above, it's invalid for various reasons50x
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.
But before you do, answer these two simple questions, otherwise you're wasting the other person's time:
- What did you expect to happen?
- 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.