Practical .NET

Really Rest: Eliminating Documentation by Implementing HATEOAS

I should make it clear at the start of this column that I loathe, loathe, loathe software documentation. My objection is not to creating it (after all, I'm a part-time technical writer) but rather software documentation's general uselessness: Most documentation is never referred to. And, before I'm inundated with comments about how some piece of documentation saved your life, let me point out that the reason you remember that piece of documentation helping is because documentation being useful is an unusual event.

To be genuinely useful, documentation must be both unambiguous and contribute directly to some task that developers have to perform. If, for example, we build a service and want to communicate information to developers building consumers, useful documentation would remove ambiguity and be delivered in a format that would be useful in writing the consumer's code. If this were done right, the need for "documentation" in the traditional sense of "something written by developers" might disappear completely.

For REST applications the critical information needed to create a consumer includes the relevant URLs and the message formats expected by the service. Communicating message formats in a useful way is already taken care of. In the bad old days, when messages were in XML, XML Schema provided a way of communicating message formats. These days, messages are typically in JSON so JSON Schema is your best choice for communicating message formats.

Communicating Endpoints
Which still leaves the question of how to communicate the URLs to be used by the consumer. At the very least, one endpoint -- the "entry point" to the service -- needs to be provided. That entry point might, for example, be the URL to be used when adding a new item to the service or, perhaps, the URL used to retrieve a list of items. After that, though, it would be useful if the developer could be given the other URLs the service supports, along with some direction on when to use them, through the service itself.

That's the point of HATEOAS (the unbelievably awkward acronym for Hypertext As The Engine Of Application State). To quote Wikipedia, HATEOAS is built on the idea that "A REST client needs no prior knowledge about how to interact with an application or server beyond a generic understanding of hypermedia."

Practically, this means a full response to any request to a Web Service would include not only the output from the operation (the response message accompanied by an HTTP status code) but also all the endpoints a consumer might want to use next. For a query, it might include the URLs to add a new item or to update/delete the item returned in the query.

It's always tempting, in these discussions, to assume that the related links are just the basic CRUD operations (Create, Read, Update, Delete). While, in the end, all business operations are reduced to those changes to data, it requires some distortion to the business processes to require the consumer to think of every business operation that way. It's not going to be possible to eliminate documentation if we also require developers to rethink processes using some paradigm that doesn't match the business.

A Ticketing Case Study
As an example, I've been working with an application recently that manages tickets to events. The first entry point to this system is a GET query that returns a list of events I can buy tickets for. I can then purchase tickets, cancel those tickets, transfer those tickets to someone else and accept tickets transferred to me. While it might be possible to recast those business operations into CRUD terms (cancel = delete), it would merely create additional barriers to the developers trying to create consumers for the ticketing service.

But there may be no need to create documentation at all: If all the consumers are built by the same team that builds the service, documentation shouldn't be necessary. In that scenario, team members can communicate with one another, review one another's code and consult among themselves (it's not cheating to talk to other members of the team, though documentation mavens sometimes imply that it is). The consumer's code should be written so that, as developers leave the team and others arrive, the code explains itself (what I've called Really Obvious Code or ROC).

But, assuming you want to support developers creating consumers on different teams, the alternative to consultation is HATEOAS: return links to other common operations as part of your response to any request.

HATEOAS in Action
As an example of HATEOAS from my ticketing application, a request for events might also include the links that allow a consumer to buy tickets for those events. The following JSON message might, for example, be the one returned from a request for events between two dates. The first part of the message is an array of Event objects (one for each event in the time period), followed by a links property. That links property contains an array of links flagged as "purchase," one for each event:

{
   "Events": [
      {
         "Id": 1953,
         "Name": "Bruce Cockburn",
         "Date": "02/03/2019",
         "Location": "Vogel Hall",
         "Cost": 35.25
      },
      {
         "Id": 1954,
         "Name": "TafelMusik",
         "Date": "02/04/2019",
         "Location": "Vogel Hall",
         "Cost": 35.25
      }
   ],
   "links": {[{
            "rel": "purchase",
            "href": "https://www.vogelhall.com/1953"
         },
         {
            "rel": "purchase",
            "href": "https://www.vogelhall.com/1954"
         }
      ]}}

In the same way, when a user buys tickets, the response might include links to the related activities that can be performed with a ticket: refresh, transfer and cancel. That message might look like this:

{"ticket":{
   	    	"ticketId": "195301",
   	    	"status": "Active",
            	"scanCode": "http://www.vogelhall.com/ticket/195301",
		"Event": {...omitted for space...}
   	  },
"links": {[{
            "rel": "self",
            "href": "https://www.vogelhall.com/195301"
         },
         {
            "rel": "cancel",
            "href": "https://www.vogelhall.com/195301"
         },
         {
            "rel": "transfer",
            "href": "https://www.vogelhall.com/[email protected]"
}]}

Since two of the URLs in the links section look alike, it's worth discussing how they would be used. The URL labeled "self" would be used with the GET verb to refresh the ticket to fetch the ticket information. On the other hand, the DELETE verb would be used with link marked "cancel" to cancel the tickets. Only a cursory knowledge of the HTTP protocol on the part of the consumer's developers would be required.

Obviously, this process wouldn't completely eliminate the need for some kind of documentation. If I were developing a consumer, it's not clear to me whether I should use PUT or POST with the cancel link. In fact, a case might be made for using DELETE with a transfer because, after all, the current owner is losing the tickets.

Nor is the intent to automate the process of using those additional links -- a developer would need to scan the links section of a response message to determine what links should be used when writing the code to perform those transactions.

As the item changes state, HATEOAS requires that the links section change. For example, once a ticket is canceled it can't be transferred but (depending on how the application works) it might still be retrieved. In that case, the links section for the ticket would still include the self link. The response message for the cancel operation would, then, look like this:

{"ticket":{
   	    	"ticketId": "195301",
   	    	"status": "Cancelled",
		"Event": {...omitted for space...}
   	  },
"links": {
         [{
            "rel": "self",
            "href": "https://www.vogelhall.com/195301"
          }]}

These changes in the links section are the reason that the acronym HATEOAS includes the phrase "Of Application State": Links reflect only valid operations that can be performed on the item and those operations are driven by the item's current state.

From a developer's point of view, this is good news. It means that the links section for any response message can be built by examining the item's state after the operation completes. The code for generating the links section for any service could consist of a single method, called after each operation.

Messages and Formats
Of course, that leaves open the relationship between message formats and URLs. It may be possible to eliminate documentation around that topic (or, at least, sharply reduce it) by adopting good message strategies: Have few message formats (ideally, one) and, when processing that message, take all possible defaults and ignore extraneous information. The single message format used for purchasing, canceling, transferring and accepting tickets might look like this:

{
   "ticketId": "195301",
   "scanCode": "http://www.vogelhall.com/ticket/195301",
   "status": "active",
   "ownerId": "pvogel01",
   "event": {...}
}

For a cancel or a transfer operation, only the ticketId might be filled in. When purchasing a ticket, the request message might include just the ownerId and an event object with just the eventId filled in (this assumes that the customer's payment information is already on file). For any transaction, if more properties than the minimum required were provided by the consumer, the service could either:

  • Ignore the "extra" properties
  • Use them to provide additional validation for the request (does the event description match the eventId?)
  • Return an error message

There is often an assumption that documentation is a "free good." It is not: Documentation costs time and money to produce, time and money taken away from delivering functionality to the user. Any tool that facilitates communication between developers while reducing the time not spent on development is a good thing. HATEOAS falls into that category.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube