Open Credo

August 12, 2015 | Microservices

Designing a REST API with fine-grained resources, HATEOAS and HAL

Over the last few months one of my main projects at OpenCredo has involved creating various microservices which are interacted with via REST. We’ve been working with a relatively rich domain model, which in turn has presented a lot of challenges in how to design our various resources. This blog post aims to summarise various techniques and practices which I’ve found helpful in overcoming these challenges.

WRITTEN BY

Andrew Morgan

Andrew Morgan

Designing a REST API with fine-grained resources, HATEOAS and HAL

Business Domain

We are building an auditing system which can be used by customers to request audits on various companies if they want to know more about their business practices. For example, as a clothing retailer, I may believe that one of my factories is not complying with various employment regulations. In this case I should be able to request that an audit takes place on the factory, after which an auditor can accept the audit, investigate the factory, and highlight any issues they might find. Following this, the auditee has to resolve any issues and prove to the auditor they have done so with evidence. They can then publish the audit making it available to me and any other interested third parties to view. Below is a state diagram of the workflow:

enter image description here

Some problems with CRUD

At first glance, we see that we have just one entity that is being modified throughout this process – an audit. So one might think when designing the API that it would be a good approach to create a single audit resource with CRUD operations that translate to POST, GET, PUT and DELETE respectively.

Creating a new audit:

POST /audits 
{
    "status": "REQUESTED", 
    "requestByCompany" : "OpenCredo",
    "auditCompany" : "Audit Co Limited",
    "auditedCompany" : "Tom's Factory",
    "issues" : []
}
201 Created
Location www.audit-manager.com/audits/1

Retrieving the newly created audit:

GET /audits/1
200 OK

An idempotent modification of the audit:

PUT /audits/1
{
    "status": "ACCEPTED", 
    "requestByCompany" : "OpenCredo",
    "auditCompany" : "Audit Co Limited",
    "auditedCompany" : "Tom's Factory",
    "issues" : [
        {
            "name" : "Unsafe working conditions",
            "status" : "UNRESOLVED"
        }
    ]
}
200 OK

Deleting an audit:

DELETE /audits/1

This type of course grained, CRUD Rest API, can make life for the API consumer difficult:

  • It’s bloated and confusing. If you consult the state diagram, you’ll see that all the various possible transitions aren’t communicated at all in the API contract. In fact we’ve completely ignored all this complex logic surrounding the auditing workflow (despite it being implemented in the service), and just expected the API consumer to modify the audit directly.
  • The API consumer knows nothing about permissions. When editing an audit, the permitted changes are calculated based on the role of the user making the change, and of the current state of the audit. For example only an auditor can accept audits, and they can only do so if the audit has a state of requested – so if you tried to make an update that violated these constraints the server would give you a 403. The more complicated the state machine, the more work for the API consumer trying to figure out what changes are and are not valid, and the more room for error in allowing an end user of a client to make calls which will ultimately fail.
  • Another problem is changes to both permissions and potential interactions. If the business suddenly decides that the audit requester should be able to view an audit before it is published, then changes to the permission code would have to be performed in both the API and the API consumer. This kind of coupling is exactly the kind of thing that should be avoided with REST, as when we make a server side change we want to aim be able to do so independently of the API consumers, knowing that they will be able to adapt with no code changes.

State transititions as resources

If you look at the state diagram, you will notice that although all our changes will apply to the same resource, we still have a finite list of transitions which can take us through the various states. So, rather than modifying the resource directly we could trigger these transitions by interacting with sub resources of the audit.

From another perspective, if you speak in the ubiquitous language of our domain you’ll talk about accepting an audit, rejecting an audit, submitting issuesetc. So continuing to speak in this language with our API makes sense.

To accept an audit:

PUT /audits/1/acceptance

201 Created

To reject:

PUT /audits/1/rejection

201 Created

Submit issue:

POST /audit/1/issues
{
    "description" : "Breaking minimum wage laws"
}

201 Created
Location /audit/1/issues/1/

Resolve issue:

PUT /audit/1/issues/1/resolution
{
    "evidence" : "We've done something to fix it"
}
200 OK

Of course, all our resources are still nouns, so they themselves can be interacted with by the API consumer using the various HTTP verbs as normal:

GET /audits/1/acceptance
200 OK
{
    "acceptedBy" : "Andrew Morgan",
    "acceptedAt" : "2015-08-07T18:30:27+00:00"
}

Another thing to note, is our various endpoint payloads now only contain the data we need, with some simple endpoints such as /acceptance not requiring a payload at all. Things have become a lot less bloated than they were when we were just doing PUT with the entire audit JSON whenever we wanted to make a change.

Permissions with HATEOAS

Although the API is more fine grained, with each resource mapping to a state transition, we still don’t know what operations are possible on a resource without consulting some documentation, and if we did we wouldn’t know what subset of those operations are available based on the users permissions. This is where HATEOAS comes in, known as hypertext as the engine of application state, which uses hypermedia to dynamically convey possible API interactions to it’s various consumers. In our case, this works particularly well with permissions, as now when we GET our audit resource we can have various relation types telling us the next possible actions:

GET /audits/1
200 OK
{
    "content" : {
        "status": "REQUESTED", 
        "requestByCompany" : "OpenCredo",
        "auditCompany" : "Audit Co Limited",
        "auditedCompany" : "Tom's Factory"
        ...
    },
    "links": [ 
        {"rel" : "self", "href": "/audits/1" },
        {"rel" : "accept", "href": "/audits/1/acceptance" },
        {"rel" : "reject", "href": "/audits/1/rejection" }
    ]
}

But when we GET an audit which has a status of SUBMITTED_TO_FACTORY, and we are authenticated as the auditee, the available relation types would change:

GET /audits/1
200 OK
{
    "content" : {
        "status": "SUBMITTED_TO_FACTORY", 
        "requestByCompany" : "OpenCredo",
        "auditCompany" : "Audit Co Limited",
        "auditedCompany" : "Tom's Factory",
        "issues" : [
            {
                "content" : {
                    "name" : "Some issue",
                    "status" : "UNRESOLVED"
                }
                "links": { 
                    {"rel" : "self", "href": "/audits/1/issues/1" },
                    {"rel" : "resolve", "href": "/audits/1/issues/1/resolution" }
                },
            },
            {
                "content" {
                    "name" : "Some other issue",
                    "status" : "RESOLVED"
                }
                "links": { 
                    {"rel" : "self", "href": "/audits/1/issues/1" }
                }
            }
        ],
        ...
    },
    "links": { 
        {"rel" : "self", "href": "/audits/1" },
        {"rel" : "publish", "href": "/audits/1/publication" }
}

And of course, when performing a GET on the same audit in the same state whilst authenticated as the auditor, the links would change again because the auditor doesn’t have permission to do anything other than view it:

GET /audits/1
200 OK
{
    "content" : {
        "status": "SUBMITTED_TO_FACTORY", 
        "requestByCompany" : "OpenCredo",
        "auditCompany" : "Audit Co Limited",
        "auditedCompany" : "Tom's Factory",
        "issues" : [
            {
                "content" : {
                    "name" : "Some issue",
                    "status" : "UNRESOLVED"
                }
                "links": { 
                    {"rel" : "self", "href": "/audits/1/issues/1" },
                },
            },
            {
                "content" {
                    "name" : "Some other issue",
                    "status" : "RESOLVED"
                }
                "links": { 
                    {"rel" : "self", "href": "/audits/1/issues/1" }
                }
            }
        ],
        ...
    },
    "links": { 
        {"rel" : "self", "href": "/audits/1" },
}

As possible actions are now driven entirely by the API, we have one less concern for the consumer which no longer needs to worry about permission logic. So for example, a menu on the screen of a single page app could dynamically change by simply parsing the response and adding / removing items based on the presence of relation types. Also, changes to the permission logic will now be non-breaking to the API consumer, allowing the API to evolve independently of it’s clients. In fact the href values can also change, as long as we keep the same relation types.

One thing to note is whether or not we are breaking the semantics of REST in our relation type naming. You could argue that relation types should be named as resources, not verbs as done in this example. I think in this case there’s value in breaking these semantics, because it allows you to have different relation types for the same URL in order to communicate multiple permutations of permissions on the same resource:

"links": { 
    { "rel" : "accept", "href": "/audits/1/acceptance" },
    { "rel" : "view-acceptance", "href": "/audits/1/acceptance" }
}

It’s more of a pragmatic approach than anything, because some potential alternatives would be to make an OPTIONS request to see what http verbs are possible (meaning > 1 requests), or to add a verb attribute as an additional one alongside href which also breaks the semantics.

Just as a footnote it’s worth bearing in mind that if you are going to GET resources which will have frequently changing link relations, you may not receive the most up to date response unless you set the appropriate cache control headers in your request.

Self documenting the API

One problem we still have, is not knowing information such as possible http verbs to use on various resources and their required payloads. We also aren’t sticking to a defined hypermedia representation type, meaning we are making up our own resource structure rather than using a predefined one that various libraries may already be able to understand. This is where something like HAL could come into play:

GET /audits/1
200 OK
Content type application/hal+json
{
    ...
    "_links": { 
        "curies": [{
         "name": "audit",
         "href": "http://docs.audit.com/relations/{rel}",
         "templated": true
       }],
        "self" : { "href": "/audits/1" },
        "audit:accept" : { "href": "/audits/1/acceptance" },
        "audit:reject" : { "href": "/audits/1/rejection" }
    }
}

Whilst self is a predefined relation type, our custom ones are not. We’ve therefore had to create documentation for them, knowing our API consumers will have no prior knowledge of what they do. This is referenced by templating the href attribute in the curies element named audit to produce urls for various bits of API documentation. So we now have http://docs.audit.com/relations/accept and http://docs.audit.com/relations/reject respectively. Again this is more useful to our consumers, because they discover resources and their documentation as they become available as interactions, rather than having to spend a long time trapsing through contracts and documentation in advance.

There’s a lot more to HAL than what has been documented in this blog post, so I’d encourage you to read the spec. It’s currently in draft phase but has support from a variety of tools and frameworks. One that I’ve found useful was Spring HATEOAS.

Summary

Taking an entity and exposing it via a Rest CRUD interface is not always the best solution. I hope this has showcased:

  • The benefits of a finer level of granularity for our API consumers.
  • How using HATEOAS to drive possible interactions based on user permissions and business rules allows the API to evolve independently of it’s clients.
  • And finally how documentation provided within hypermedia using HAL can present you with an easier to digest API.

 

This blog is written exclusively by the OpenCredo team. We do not accept external contributions.

RETURN TO BLOG

SHARE

Twitter LinkedIn Facebook Email

SIMILAR POSTS

Blog