ReST, HTML, JavaScript, and URIs

Tuesday, August 26th 2014

We created data- attributes in HTML5 so servers could embed application-specific information in the structural representation of a resource. The IANA also provides a directory of meaningful link relationship types for use in HTML documents. If we comply with ReST over HTTP, then we should use those to contain the information that our JavaScript uses. Otherwise, we end up breaking the core tenets of ReST and our applications become difficult to maintain and extend.

It always starts with a conversation

I had a short conversation, today, with my good friend Bryan and newer acquaintance Dave that centered around the contents of a file named router.js that we include with require.js.

In our project, we use amplify to provide in-browser pub/sub. This provides developers with a way to make modular and composeable pages. It turns out, though, that the inclusion of amplify has led to the use of its amplify.request API, as well. And, here’s where it gets a little hairy.

In the file router.js, we find entries like the following:

1
2
3
4
amplify.request.define('Create_SomeResource', 'ajax', {
url: '/parent-resource/{id}/child-resource{childId}/create',
type: 'GET'
});

And, this is where I find fault with what we wrote: we partly hard-coded a URI template into a JavaScript file.

Why is a URI template in a JavaScript file bad?

If you want a clearer understanding of ReST, you can read Dr. Fielding’s article or, if you trust me, you can read my own post.

An application using the ReST architecture starts at a URI. (For us, we’ll just say URL from now on because that’s what we type in browsers.) Some server sends a representation of the resource to which the URL points, normally a nice HTML document. Hypermedia FTW. Now, here comes the important part:

The returned representation contains all links that define transitions valid for the current state.

That means that when you go to curtis.schlak.com, the links that you see are the only valid transitions for you to take. You may notice, for example, that my blog’s URLs take the form /yyyy/mm/dd/some-text.html. Somehow, you know the title for a post I wrote four years ago. You could infer a valid URL from that information, type it into the address bar, and transition to a new state outside the ones presented to you as part of the links in the HTML document.

That’s bad. Because endpoints change. Because URLs can point to transient resources. Because the URL specification clearly states that a URL is opaque to humans. That human-readable aspect does not need to exist.

So, if it’s not in the HTML document somewhere, then we shouldn’t assume that anything we type into the address bar would take us to a valid place on the Wild, Wild Web.

Finding URLs in the JavaScript breaks that rule of meaningful link semantics in the resource representation sent to the browser.

But, Curtis, JavaScript comes from the server, too!

True, fine reader. In our HTML document, we would have something like

1
<script src="/scripts/app/controller/view.js"></script>

That is a valid URL that comes from the resource representation. Then, the browser downloads the JavaScript representation of that URL and everything just works. So, what’s the big deal, you think.

The big deal: code-on-demand

I spend all this time talking about ReST as an architecture and URIs as ways to find resources and methods of getting representations of those resources, I sometimes forget to write about another important part of ReST: code-on-demand.

You’ve written a Web application and your code sends some HTML back to the browser and all is good. But, the Web application has to be shiny. So, to “power” the interactivity of the application, you use some JavaScript to make AJAX calls to provide a better user experience. Your application sends and receives JSON documents that contain all the data necessary to fulfill the functionality of the Web app and all is well in the world.

What purpose did JavaScript serve in this case?

Those JSON documents that you send and receive need application-level understanding to interpret the contents of the document. ReST allows for an “optional constraint” called code-on-demand that the client uses to extend its understanding of media types within its purview. Browsers don’t know how to interpret the content of a document with the MIME type application/json. To allow that to happen, your HTML loads JavaScript code to interpret the contents of those documents.

JavaScript provides extensibility to the native functionality provided by the browser, the client in the Web-based ReST architecture. Because of that, the JavaScript in our applications should interpret documents loaded in the browser, sent to an endpoint, or retrieved from the server. It should not carry meaningful links because it extends the functionality of the browser on the already-loaded resource representations.

Putting that all together

Back to the original example:

1
2
3
4
amplify.request.define('SomeResource', 'ajax', {
url: '/parent-resource/{id}/child-resource{childId}/create',
type: 'GET'
});

How do we get that hard-coded URI template out of there and replace it with something that conforms to the ReST standard? In our HTML, for example, we could go to the Link Relations list maintained by the IANA and found the “create-form” relation. Then, our HTML could contain

1
2
3
4
5
<!DOCTYPE html>
<html>
<head>
<link rel="create-form" href="/parent-resource/{id}/child-resource{childId}/create">
<!-- ... more document here ... -->

Then, the JavaScript would read like this:

1
2
3
4
amplify.request.define('Create_SomeResource', 'ajax', {
url: $('link[rel="create-form"]').attr('href'),
type: 'GET'
});

That demonstrates that the HTML should own the links (in link or a tags or in data- attributes) that the JavaScript can then act upon. Need to change your controller name? Want to move to another URL creation scheme? The JavaScript will work unchanged, at this point, and you can go to the place that generates the paylaod for the resource representation where all of the data should live!