by Jonathan Duggan, Daniel Bray
Perhaps the hardest problem to solve when writing REST APIs is managing how those APIs evolve over time. Our goal is to minimise pain for API client developers while not being completely hamstrung by early design decisions.
There are two popular approaches to this:
- Versioning Avoid forcing client code to change at the expense of requiring the server-side to support multiple API versions
- Deprecation Warn clients that they need to update by some future date
Neither of these options are ideal. However there is a third approach that, in some circumstances, can represent a better solution – hypermedia.
Hypermedia REST APIs allow developers to build services that effectively decouple client and server, allowing them to evolve independently. The representations returned for REST resources contain not only embedded data, but also links to related resources.
We’ll focus on this approach and use of the Hypertext Application Language (HAL) convention to document a REST API. By way of example, we’ll show how existing Angular libraries can be used to build a UI that is stable under what would normally be a breaking API change.
We need to talk about REST
For most developers, REST means ‘JSON over HTTP’. This is mostly right, since it’s stateless, client/server, cacheable and layered, but it does omit of the more complicated elements of the uniform interface constraints, specifically Hypermedia as the engine of application state (HATEOAS).
To be fair, if you’re writing your own UI against your own back-end, with a smallish internal team, this is something you can probably ignore. It’s likely to be more trouble than it’s worth. For this discussion though we’re talking about writing an API that will be shared with multiple 3rd party organisations where the impact of making non-backwards-compatible changes will be significant.
What’s the Problem?
The root of the problem lies in hard-coding URIs in client code. For example, if your client does something like this:
function ConfigService($resource, BASE_URLS) {
var baseUrl = BASE_URLS.devUrl,
resourceUrl = baseUrl + 'configs/:id;
return $resource(resourceUrl, {}, {
'getConfig': {
method: 'GET',
params: {
action: 'details'
}
},
'getStatus': {
method: 'GET',
params: {
action: 'status'
}
}
});
}
This code is assuming that, for some element id
the URI to get the status is always going to be configs/:id/status
. As a result, as soon as we change that URI on the server, clients will be break.
How does Hypermedia Help?
Further to simply having a REST API that adopts the principles of a hypermedia API, we need to specify a format that our API will follow. By standardising this format, or hypermedia type, multiple clients can easily integrate with the API.
The most widely used hypermedia types are HAL, Collection+JSON, Siren, JSON API, and Hydra and JSON-LD. For this post, we’ll take a closer look at HAL.
As a hypermedia format, HAL provides a consistent and straightforward mechanism to link resources in an API. Its goal is to make an APIs structure easily understandable and all related resources discoverable.
HAL’s design focuses on two things: resources and links. A resources can contain an embedded resource or a link to another resource. It is designed with the goal of building APIs in which clients can navigate resources by following links.
Considering the earlier example, this would mean returning something like the following within the response to GET configs/:id
.
{
"id" : 123,
"name": "Hello World",
"content" : {},
// HAL content
"_links" : {
"self" : {
"href" : "/config/123"
},
"details" : {
"href" : "/config/123/details"
},
"status" : {
"href" : "/config/123/status"
}
}
}
Using such a structure clients can avoid hard-coding the details
or status
URIs and can instead inspect _links
. For example, to get the status the client would inspect the response for the value of _links.status.href
and then request that resource. This can be called directly, without any further inspection, since it will always point to the correct URI – regardless of whether it has changed by the server team.
Sounds Like More Work
Well it would be, if it were not for that fact that there are many popular libraries that can take care of the details. For example Spring Boot has HATEOAS baked into a starter.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
Usage is straightforward:
- Extend entities from
org.springframework.hateoas.ResourceSupport
This places no requirements on your class other than requiring that you they can’t have agetId
method - In controllers set the links before returning the resource
@RequestMapping(path = "/configs/{id}", method = RequestMethod.GET)
public HttpEntity<Config> survey(@PathVariable("id") Long id) {
// Config extends ResourceSupport
Config config = configService.getConfig(id);
// Link to self
config.add(linkTo(methodOn(ConfigController.class)
.config(config.getConfigId())).withSelfRel());
// References methods on ‘this’, or on other @RestController services
config.add(linkTo(methodOn(ConfigController.class)
.details(config.getConfigId())).withRel("details"));
config.add(linkTo(methodOn(StatusController.class)
.status(config.getConfigId())).withRel("status"));
return new ResponseEntity<>(config, HttpStatus.OK);
}
Note that Spring derives the URIs from the @RequestMapping
on the referenced methods.
Consuming HAL on the Client Side
There are many libraries to consume HAL responses on the client side – since we develop a lot of Angular UI where we’ll use angular-hal.
A sample controller might look something like the following:
angular
.module('app', [
angularHal.default
])
.factory('$api', function ApiFactory($http) {
return $http({
url: '/configs'
});
})
.controller('ConfigController', function ConfigController($api, $scope) {
// Gets a config's status
this.getStatus = (config) => {
config
.$request().$del('status')
.then(() => load());
};
// Delete a config using the 'self' link
this.deleteConfig = (config) => {
config
.$request().$del('self')
.then(() => load());
};
return this;
});
;
Supporting Major API Changes
The changes shown in the example shown above are simple – they show protection against minor API changes, for example renaming URLs or inserting additional parameters into the API. What if an API requires more invasive changes?
In the above example we have a config resource, and a second call to get the status. Imagine, after some user-testing, we noticed that clients always wanted the status whenever we loaded the config. In this case it would make sense to include the status in the response, but how can we do this without breaking existing clients?
The easiest way is to use embedded resources:
- Include
status
as a simple JSON property so that new clients can use it as a normal value - Include
status
in an_embedded
section. This is a little more complicated than the simple JSON properly, but it has the benefit that the angular-hal snippet above will work as-is, with no amendments.
{
"id" : 123,
"name": "Hello World",
"status": {
"name": "OK",
"last_amended": "1992-01-12T20:17:46.384Z"
},
// HAL content
"_links" : {
"self" : {
"href" : "/config/123"
},
"details" : {
"href" : "/config/123/details"
},
// Amended resource. Status moved from a link to an embedded value
"_embedded": {
"status": {
"name": "OK",
}
}
}
}
We have changed the JSON resource we’re returning as well as the changing the expected client behaviour (it no longer needs to do a second GET to retrieve a config’s status). However, angular-hal will know that when it’s asked to get the data for status
it will use the embedded value from the original response. As a result, any UI written with angular-hal will accommodate these changes without requiring a rewrite.
So, should I use this?
In many cases hypermedia APIs can be overkill, but where multiple 3rd party client applications are involved, and if the API is something you need to maintain for years, HAL can be a benefit.
As ever, there are pros and cons so good judgement is required.
Pros
- Client apps can be built without any URL dependencies
- Provides a clear and easy way to manage API versioning
- Simpler forward and backward compatibility through component decoupling
- Accommodate fundamental object model changes in APIs
- Manage API call processing times better with embedded or linked resources
Cons
- Can be time consuming to setup and maintain a hypermedia driven API
- As hypermedia popularity increases though, so to has the availability of 3rd party libraries to simplify development.
- Hypermedia APIs tend to be less readable than traditional APIs
- Developers may feel the urge to optimise their clients and take shortcuts – essentially defeating the purpose of implementing hypermedia in the first place.
Top comments (0)