Rob Conery asked for some feedback on an API he has been building for Tekpub. I know that Rob asked for URLs, and URLs he shall get, but I like to get a better understanding of the relationships between my resources before I start minting URLs for my service.
On a number of occasions I have found a diagram like the one below is useful for visualizing an API. It lets me see how a client will surf the API to get at the information it wants. This is not any kind of official diagramming methodology. I just use class diagrams and use the associations to represent links between resources and the classes represent a “kind of resource”. I use that vague term because one box in this diagram may represent multiple actual resources in our real system.
This is definitely not the only way to create these relationships and I’m not even going to claim that it’s the best way. However, it seems to fit the requirements as I understand them.
The example exchanges that I show below are using URLs that are based on my preferences, if you don’t like them, change them, the only thing it might affect is caching. The client doesn’t care about the URL structure, it only cares about link relations, which are identified as URNs that look like urn:tekpub.*. Your server framework may care about your URI structure, pick whatever works for you.
The client interacts with the API by first retrieving the root document.
GET / => 200 OK Content-Type: application/hal+json Cache-Control: max-age=86400 { "_links": { "self": { "href": "/" }, "urn:tekpub:login": { "href": "/login" } } }
This representation is the same for all users as no credentials have been provided so far. It can be safely cached for a reasonably long period of time. There is no point having the server constantly re-serve up this content.
I chose to use application/hal+json as a media type because I am guessing Rob will prefer looking at curly braces over angle brackets, and I need some media type that has semantics for links. Hal is a good general purpose media type that allows us to use link relations to define all of our application semantics. If you want to see an alternative way of doing this with a custom media type, take a look at Mike Amundsen’s post. And Mike Kelly, the author of Hal, created his variant of Rob’s API here.
The client can be programmed to go looking for the urn:tekpub:login and make a request using the username and password provided by the user.
GET https://api.tekpub.com/login Authorization: Basic VEFWSVNcZGFycmVsOg== => 200 OK Content-Type: application/hal+json Cache-Control: no-store
{"_links": { "self": { "href" : "/Login"}, "urn:tekpub:userhome" : { "href" : "/user/95/home"} }, "Message" : "Welcome Joe", "AccessToken" : "vF9dft4qmT" }
I chose the “no-store” caching directive for this response because I don’t want anyone else to retrieve a cached copy of this user’s access token.
Once the access token has been placed in the authorization header, the client can follow the link to the urn:tekpub:userhome resource.
GET /user/95/home Authorization: tekpubtoken vF9dft4qmT => 200 OK Content-Type: application/hal+json Cache-Control: max-age=86400 { "_links": { "self" : { "href" :"/user/95/home" }, "urn:tekpub:allproductions" : { "href" : "/user/95/allproductions", "title" : "All productions" }, "urn:tekpub:newepisodes" : { "href" : "/episodes/new", "title" : "New Episodes" }, "urn:tekpub:productionsbycategory" : { "href" : "/productions{?category}" } "urn:tekpub:categories" : { "href" : "/categories" } } }
This resource also has a nice long max age so that the client can continue to refer back to this resource to access the links without having to round-trip. I included the user id in the URI to make caching a little easier. If you really don’t like the user in the URL and you still want to cache this response then you will probably need to add a vary header that causes the authorization header to be used as part of the cache key, and then you will want to make sure you only use private caching.
If the user chooses to look at all productions, then the client should pull out the url from the urn:tekpub:allpublications link and follow it. This link includes the user id again because the “allowed” property is dependent on who the user is.
GET /user/95/allproductions Authorization: tekpubtoken vF9dft4qmT => 200 OK Content-Type: application/hal+json Cache-Control: private, max-age=30 { "_links" : { "self" : { "href" : "/user/95/allproductions" }, }, "name" : "Joe", "_embedded" : { "urn:tekpub:production" : [ { "_links": { "self": { "href": "/production/22" }, }, "allowed" : "true", "title" : "Rails runs rings round ReST resource representations" }, { "_links": { "self": { "href": "/production/74" }, }, "allowed" : "false", "title" : "Surfing the waves beats surfing an API" } ] } }
This representation contains an array of embedded production resources. The documentation of the urn:tekpub:production link relation should state that “allowed” and “title” are valid properties of production resource and can be embedded in a parent resource document. The client can know to look for the allowed property because of the urn:tekpub:production link relation. This is an important feature of the Hal media type. The link relations are heavily overloaded to provide all kinds of semantic information. This allows the media type to remain generic. One characteristic of a REST API is that semantic information that is shared between the client and the server is limited to either the link relation definition or the media type definition. You can put it in either, just don’t put it elsewhere.
This response is cached just for a short period (30secs) just so that when people are zipping around in their app, the client will not be continually asking for the server for the same resource. Obviously this number can be tuned based on how often you expect this data to change and how long you think a user can stand having stale data.
If instead of selecting the urn:tekpub:allpublications link, the user selected the urn:tekpub:productionsbycategory link then the client needs to know to complete the URI template with the appropriate category. The urn:tekpub:categories link provides the client with a way to determine the available categories. The representation that is returned from the urn:tekpub:productionsbycategory will be functionally equivalent to the one returned by urn:tekpub:allpublications the only difference being which productions are included in the list.
When a user chooses which production they are interested in, we follow the urn:tekpub:production link to get the representation. Unless there needs to be user specific data or statistical data in here, I would expect this can be cached for a long time. I am assuming that the final urn:tekpub:content would verify the authorization header to ensure that the user is actually allowed to access that content.
GET /production/22 Authorization: tekpubtoken vF9dft4qmT => 200 OK Content-Type: application/hal+json Cache-Control: max-age=86400 { "_links" : { "self" : { "href" : "/production/22" }, }, "title" : "Rails runs rings round ReST resource representations", "_embedded" : { "urn:tekpub:episode" : [ { "_links": { "self": { "href": "/episode/745" }, "urn:tekpub:content" : { "href" : "/content/745.mp4" } }, "title" : "PATCH or PUT that is the question", }, { "_links": { "self": { "href": "/episode/921" }, "urn:tekpub:content" : { "href" : "/content/921.mp4" } }, "title" : "RESTful URL - The mythical creature" } ] } }
The last step would be to allow the user to select the content and client would render the content by accessing the representation at the urn:tekpub:content link.
Hopefully, this walkthrough did not make this relatively simple example more complicated that it actually is. The unfortunate part of describing a hypermedia API is that you actually need to show example responses to show the links that are contained within it. That’s one reason why I find the diagrams quite useful for visualizing the API.
The final point is that by using the urn:tekpub:* link relations to describe the API then we can avoid having the client take hard dependencies on those URIs and that gives us certain flexibilities that will help us to evolve our API over the long term. This level of indirection that we are introducing is a short term pain, long term gain strategy. This assumes of course that one of your goals is to be able to independently evolve your clients and servers.