Chapter 9. Hypermedia and Microservices

“Tricks? Gadzooks, Madam, these are not tricks! I do magic. I create, I transpose, I transubstantiate, I break up, I recombine—but I never trick!”

Merlin, The Seven Faces of Dr. Lao

Microservices began to surface as a discussion topic in late 2013. The first extensive writing about it appeared in March 2014 on Martin Fowler’s blog. At that time, Fowler offered this definition of microservices:

The microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API.

This definition touches many of the hot buttons people have in mind when they think of APIs on the Web:

  • “A suite of small services” describes the collection of one or more single-capability components.

  • “Each running in its own process” points out that each capability is a standalone service.

  • “Often with an HTTP resource API” identifies the implementation details directly.

While there is some disagreement on the fine points of this definition (must microservices only communicate over HTTP?, what constitutes “small”?, etc.) there is wide agreement on the general pattern: a set of services loosely connected using common application-level protocols.

In fact, another definition of microservices that I think is helpful sounds a bit like my description and it is the one offered by Adrian Cockcroft:

Loosely-coupled service-oriented architecture with bounded contexts.

Cockcroft’s definition side-steps the “small” and “HTTP API” aspects of microservices and focuses on something else—bounded context. This phrase comes from Eric Evans’ work on domain-driven design (DDD). Evans’ notion of focusing on the problem domain and making that the design canvas is very powerful. It follows rather closely the kinds of things I covered in Chapter 5, The Challenge of Reusable Client Apps, too. So, as you might imagine, Cockcroft’s ideas have influenced the way I designed and implemented the simple microservice examples in this book.

Finally, in the book Microservice Architecture (O’Reilly), my API Academy colleagues and I came up with what I think is a very handy definition:

A microservice is an independently deployable component of bounded scope that supports interoperability through message-based communication. Microservice architecture is a style of engineering highly automated, evolvable software systems made up of capability-aligned microservices.

Two big points to notice in this latest attempt:

  • Each microservice is independently deployable and has a bounded scope (à la Evans’s DDD) that passes messages.

  • A collection of capability-aligned and evolvable components makes up a microservice architecture.

We know from previous work in this book that messages are what hypermedia APIs are all about. Messages carry the API data and the metadata needed to describe the Objects, Addresses, and Actions of the service. And in this chapter, we’ll see that a single client app that understands one or more message models is able to knit together a set of seemingly unrelated capabilities (in the forms of one or more deployed microservices) and create a unique application that is more than the sum of its (microservice) parts. Even better, this application can continue to work without breaking as the various microservices evolve over time.

The Unix Philosophy

There is a parallel between the new microservice pattern of the 2010s and the UNIX operating system from the 1970s. In the foreword for the 1978 edition of Bell Labs’ “UNIX Timesharing System” documentation, McIroy, Pinson, and Tague offer the following four points as a set of “maxims that have gained currency among the builders and users of the UNIX system.”

  1. Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new features.

  2. Expect the output of every program to become the input to another, as yet unknown, program. Don’t clutter output with extraneous information. Avoid stringently columnar or binary input formats. Don’t insist on interactive input.

  3. Design and build software, even operating systems, to be tried early, ideally within weeks. Don’t hesitate to throw away the clumsy parts and rebuild them.

  4. Use tools in preference to unskilled help to lighten a programming task, even if you have to detour to build the tools and expect to throw some of them out after you’ve finished using them.

These maxims have made their way into the microservice world to varying degrees. The first one (“do one thing well”) is commonly seen in microservices. That’s why we see lots of small components, loosely connected over HTTP (although other protocols are used for microservices, too).

The second maxim (“Expect the output … to become the input…”) is also an important element—especially when you consider that the output of hypermedia APIs uses a structured format like HAL, Siren, Cj, etc. that can be reliably read by other service consumers on the Web. This is one of the reasons these formats offer so much promise in the microservice environment—they make it easy for services that have never before met to successfully share inputs and outputs.

The other two maxims talk more about developer/designer behavior (“Design … to be tried early” and “build the tools.”) than about any specific pattern or implementation detail, but they are still valuable points.

So, with these two perspectives as a backdrop, let’s see what happens when we turn our TPS web API into a set of loosely coupled microservices.

The TPS Microservices at BigCo

Since our example API is so simple, it is easy to recognize some viable context boundaries for re-engineering it as set of standalone microservices. A more rich and complex API might offer a bit more challenge when trying to locate “bounded contexts,” but ours does not—and that’s just fine.

Here is the set of microservices I’ll implement for this chapter:

Task Service

This provides all the capabilities to manage task objects.

User Service

This will be our standalone user object manager.

Note Service

This is where we’ll manage all the content notes.

In addition to these three services, I’ll create one more: the Home Service. The Home Service will act as the system root and provide connections to all the other services as needed. The Home Service will host the client-side app that will act as the consumer of the other three services.

To keep things interesting, I’ll make sure each of the three primary services support only one hypermedia format. The Task Service will reply in Collection+JSON. The User Service will only speak Siren. And the Note Service will converse in HAL messages. That means our Home Service will need to be able to talk in multiple languages in order to successfully interact with all the other components. This one-to-one linking between services and formats is a bit of a contrived example, but the general problem (that services speak varying languages at runtime) is a very real one.

In the next few sections, I’ll do a quick review of the APIs for each microservice and then spend a bit more time on the Home Service that consumes the others. It’s in that final API consumer that we’ll see how we can leverage all the client libraries we’ve built up so far and bring them together into a single multilingual API client.

The Tasks Service with Collection+JSON

The Task Service implementation has the same functionality as the one that exists in the TPS web API. The only substantial difference is that I’ve stripped out support for all the other objects (Users and Notes). Most all the code is the same as what you’ve seen in the previous chapters and I won’t review that here. I will, however, show some snippets from the initial API server router (app.js) to highlight changes to make this a single-capability service.

Tip

The source code for the standalone Task Service client can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

Following is a snippet from the ms-tasks project’s app.js file—the root file of the Task Service:

var port = (process.env.PORT || 8182); 1
var cjType = "application/vnd.collection+json"; 2

var reRoot = new RegExp('^/$','i'); 3
var reFile = new RegExp('^/files/.*','i');
var reTask = new RegExp('^/task/.*','i');

// request handler
function handler(req, res) {
  var segments, i, x, parts, rtn, flg, doc, url;

  // set local vars
  root = '//'+req.headers.host;
  contentType = contentType;
  flg = false;
  file = false;
  doc = null;

  // default to Cj 4
  contentAccept = req.headers["accept"];
  if(!contentAccept || contentAccept.indexOf(cjType)!==-1) {
    contentType = contentType;
  }
  else {
    contentType = cjType;
  }
  ...
}

The highlights of this snippet are:

1

Set the listener port for this service. I used 8184 for my local running instances.

2

Set the media type for this service to be application/vnd.collection+json.

3

Set up the routing rules for root, task, and general file requests.

4

Force the incoming accept header to result in application/vnd.collection+json.

A more robust way to handle content negotiation might be to simply return "415 - Unsupported media type" if a client makes a call that does not resolve to application/vnd.collection+json, but it is common (and well within the HTTP specifications) for services to ignore incoming accept headers and just blurt out the only format they know. The client needs to account for that, too.

The other app.js snippet worth seeing here is the one that handles the routing of requests:

...
  // parse incoming request URL 1
  parts = [];
  segments = req.url.split('/');
  for(i=0, x=segments.length; i<x; i++) {
    if(segments[i]!=='') {
      parts.push(segments[i]);
    }
  }

  // re-direct / to /task/ 2
  try {
    if(flg===false && reRoot.test(req.url)) {
      handleResponse(req, res,
       {code:302, doc:"", headers:{'location':'//'+req.headers.host+"/task/"}});
    }
  }
  catch (ex) {}

  // task handler 3
  try {
    if(flg===false && reTask.test(req.url)) {
      flg = true;
      doc = task(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // file handler 4
  try {
    if(flg===false && reFile.test(req.url)) {
      flg = true;
      utils.file(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // final error 5
  if(flg===false) {
    handleResponse(req, res, utils.errorResponse(req, res, 'Not Found', 404));
  }
...

And the key points are:

1

Parse the incoming URL into a collection of parts for everyone to deal with.

2

If the call is to the root, redirect the client to the /task/ URL.

3

Handle any /task/ requests.

4

Handle by /file/ requests.

5

Emit a 404 error for any other requests.

What is not shown here is the Task connector and component code as well as the Cj representor code. We’ve seen that already and you can check out the source code for details.

Now, with this service up and running, the response you get when you send a direct request to the /task/ URL looks like this:

{
  "collection": {
    "version": "1.0",
    "href": "//localhost:8182/task/",
    "title": "TPS - Tasks",
    "content": "<div>...</div>",
    "links": [
      {
        "href": "http://localhost:8182/task/",
        "rel": "self task collection","prompt": "Tasks"
      }
    ],
    "items": [
      {
        "rel": "item","href": "//localhost:8182/1l9fz7bhaho",
        "data": [
          {"name":"id","value":"1l9fz7bhaho","prompt":"ID","display":"true"},
          {"name":"title","value":"extensions","prompt":"Title",
            "display":"true"},
          {"name":"tags","value":"forms testing","prompt":"Tags",
            "display":"true"},
          {"name":"completeFlag","value":"true","prompt":"Complete Flag",
            "display":"true"},
          {"name":"assignedUser","value":"carol","prompt":"Asigned User",
            "display":"true"},
        ],
        "links": [
          {
            "prompt": "Assign User","rel": "assignUser edit-form",
            "href": "//localhost:8182/task/assign/1l9fz7bhaho"
          },
          {
            "prompt": "Mark Completed","rel": "markCompleted edit-form",
            "href": "/localhost:8182/task/completed/1l9fz7bhaho"
          },
          {
            "prompt": "Mark Active","rel": "markActive edit-form",
            "href": "//localhost:8182/task/active/1l9fz7bhaho"
          }
        ]
      }
      ...
    ],
    "queries": [...],
    "template": {
      "prompt": "Add Task","rel": "create-form //localhost:8182/rels/taskAdd",
      "data": [
        {"name": "title","value": "","prompt": "Title","required": true},
        {"name": "tags","value": "","prompt": "Tags"},
        {"name": "completeFlag","value": "false","prompt": "Complete"}
      ]
    }
  }
}

So, we have a Task Service up and running. Let’s move on to the User Service next.

The User Service with Siren

Just like the Task Service, the User Service I created is a single-capability microservice that allows consumers to manipulate User objects via the web API. This time, I rigged the User Service to only speak Siren. So all consumers will get Siren responses.

Tip

The source code for the standalone User Service client can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

And the top app.js file looks like this:

var port         = (process.env.PORT || 8183); 1
var sirenType    = "application/vnd.siren+json"; 2

var csType       = '';
var csAccept     = '';

// routing rules
var reRoot = new RegExp('^/$','i');
var reFile = new RegExp('^/files/.*','i');
var reUser = new RegExp('^/user/.*','i');

// request handler
function handler(req, res) {
  var segments, i, x, parts, rtn, flg, doc, url;

  // set local vars
  root = '//'+req.headers.host;
  contentType = sirenType;
  flg = false;
  file = false;
  doc = null;

  // we handle siren here 3
  contentAccept = req.headers["accept"];
  if(!contentAccept || contentAccept.indexOf(sirenType)!==-1) {
    contentType = contentType;
  }
  else {
    contentType = sirenType;
  }
  ...
}

Note the use of a new local port (1) of 8183, the setting of the default media type to application/vnd.siren+json (2), and the forcing of the accept header to the siren media type (at 3).

The other User Service snippet (shown next) is the routing code which includes:

1

Parsing the URL.

2

Redirecting root requests to /user/.

3

Handling User calls.

4

Handling any calls to the '/files/ URL space.

5

Emitting 404 - Not Found for everything else.

  // parse incoming request URL 1
  parts = [];
  segments = req.url.split('/');
  for(i=0, x=segments.length; i<x; i++) {
    if(segments[i]!=='') {
      parts.push(segments[i]);
    }
  }

  // re-direct / to /user/ 2
  try {
    if(flg===false && reRoot.test(req.url)) {
      handleResponse(req, res,
       {code:302, doc:"", headers:{'location':'//'+req.headers.host+"/user/"}});
    }
  }
  catch (ex) {}

  // user handler 3
  try {
    if(flg===false && reUser.test(req.url)) {
      flg = true;
      doc = user(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // file handler 4
  try {
    if(flg===false && reFile.test(req.url)) {
      flg = true;
      utils.file(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // final error 5
  if(flg===false) {
    handleResponse(req, res, utils.errorResponse(req, res, 'Not Found', 404));
  }
}

As you would expect, the responses from the User Service look like this:

{
  "class": ["user"],
  "properties": {
    "content": "<div>...</div>"
  },
  "entities": [
    {
      "class": ["user"],
      "href": "//localhost:8183/user/alice",
      "rel": ["item"],
      "type": "application/vnd.siren+json",
      "id": "alice",
      "nick": "alice",
      "email": "[email protected]",
      "password": "a1!c#",
      "name": "Alice Teddington, Sr.",
      "dateCreated": "2016-01-18T02:12:55.747Z",
      "dateUpdated": "2016-02-07T04:43:44.500Z"
    }
    ...
  ],
  "actions": [
    {
      "name": "userFormAdd","title": "Add User",
      "href": "http://rwcbook11.herokuapp.com/user/",
      "type": "application/x-www-form-urlencoded",
      "method": "POST",
      "fields": [
        {"name": "nick","type": "text","value": "","title": "Nickname",
          "required": true, "pattern": "[a-zA-Z0-9]+"},
        {"name": "email","type": "email","value": "","title": "Email"},
        {"name": "name","type": "text","value": "","title": "Full Name",
          "required": true},
        {"name": "password","type": "text","value": "","title": "Password",
          "required": true,"pattern": "[a-zA-Z0-9!@#$%^&*-]+"
        }
      ]
    }
    ...
  ],
  "links": [
    {
      "rel": ["self","user","collection"],
      "href": "http://locahost:8183/user/",
      "class": ["user"],"title": "Users",
      "type": "application/vnd.siren+json"
    },
    {
      "rel": ["profile"],
      "href": "http://rwcbook17.herokuapp.com/user/", 1
      "class": ["user"],"title": "Profile",
      "type": "application/vnd.siren+json"
    }
  ]
}

Note the appearance of the profile link in the response (1). This is a reference to the POD extension we created in Chapter 6, Siren Clients to make it easier to pass domain object information to Siren responses.

Now we need one more base-level service—the Notes Service.

The Notes Service with HAL

I implemented the Notes Service as a standalone single-capability component that speaks HAL. Like the others, the only thing I needed to do to create the service is to strip down the app.js file to only respond to /note/ calls and only return HAL-formatted responses.

Tip

The source code for the standalone Note Service client can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

Here’s the top of the app.js file with the new port (8184 at 1), default media type (2), and accept processing (at 3).

// shared vars
var port         = (process.env.PORT || 8184); 1
var halType     = "application/vnd.hal+json"; 2

var csType       = '';
var csAccept     = '';

// routing rules
var reRoot = new RegExp('^/$','i');
var reFile = new RegExp('^/files/.*','i');
var reNote = new RegExp('^/note/.*','i');

// request handler
function handler(req, res) {
  var segments, i, x, parts, rtn, flg, doc, url;

  // set local vars
  root = '//'+req.headers.host;
  contentType = halType;
  flg = false;
  file = false;
  doc = null;

  // it's a HAL world 3
  contentAccept = req.headers["accept"];
  if(!contentAccept || contentAccept.indexOf(htmlType)!==-1) {
    contentType = contentAccept;
  }
  else {
    contentType = halType;
  }

The routing code also should look very familiar now:

  ...
  // parse incoming request URL
  parts = [];
  segments = req.url.split('/');
  for(i=0, x=segments.length; i<x; i++) {
    if(segments[i]!=='') {
      parts.push(segments[i]);
    }
  }

  // handle options call
  if(req.method==="OPTIONS") {
    sendResponse(req, res, "", 200);
    return;
  }

  // handle root call (route to /note/)
  try {
    if(flg===false && reRoot.test(req.url)) {
      handleResponse(req, res,
        {code:302, doc:"", headers:{'location':'//'+req.headers.host+"/note/"}}
      );
    }
  }
  catch (ex) {}

  try {
    if(flg===false && reNote.test(req.url)) {
      flg = true;
      doc = note(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // file handler
  try {
    if(flg===false && reFile.test(req.url)) {
      flg = true;
      utils.file(req, res, parts, handleResponse);
    }
  }
  catch(ex) {}

  // final error
  if(flg===false) {
    handleResponse(req, res, utils.errorResponse(req, res, 'Not Found', 404));
  }
}

And the Note Service responds in HAL, as expected (this is an abbreviated display):

{
  "_links": {
    "collection": {
      "href": "http://localhost:8184/note/",
      "title": "Notes","templated": false},
    "note": {"href" : "http://localhost:8184:/note-note"},
    "profile": {"href": "http://localhost:8184/note.pod"},
  },
  "content": "<div>...</div>",
  "related": {"tasklist": [ ... ]},
  "id": "aao9c8ascvk",
  "title": "Sample",
  "text": "this note was created using the Note microservice for the TPS API.",
  "assignedTask": "1l9fz7bhaho",
  "dateCreated": "2016-02-13T19:26:25.686Z",
  "dateUpdated": "2016-02-13T19:26:25.686Z"
}

Notice the profile link that appears in the response. This is a reference to the same POD extension that was added to the Siren representation (see “The POD Specification”) and the collection and note links that can be used to make HAL-FORMS calls (see “The HAL-FORMS Extension”). Now, this service’s HAL response is providing Object metadata (via POD), Action metadata (via HAL-FORMS), and the Addresses (via the native HAL _link array).

So, that’s all the base-level microservices. The real work is in the next section. That’s when we’ll modify the Home Service to serve up the TPS client that is able to talk in multiple languages.

One Client to Rule Them All

So, when considering the challenge of creating a single client that can properly interact with multiple microservices, with each service selecting their own representation format, the real question is:

“How hard is it to create a single client that can successfully ‘speak’ multiple formats?”

It turns out that it is not all that difficult. Especially since we’ve already done the important work of creating standalone libraries for parsing HAL, Siren, and Cj. All that is needed now is a bit of rearranging, a few snips here and there, and the client libraries we’ve been working on individually can fit together nicely in a single package.

First, we’ll look at the Home Service to see just what role that server-side component plays in all this, and then we’ll drill down in the new multilingual client that brings everything together.

The Home Service

The Home Service I created has two important jobs:

  • Act as a gateway for all the other microservices (Tasks, Users, Notes).

  • Serve up the standalone multiformat client that is capable of consuming the APIs of those microservices.

First, when dealing with loosely coupled microservices, it is hard for any one component to know the details about all the others. Instead, most microservice implementations rely on a proxy or gateway to resolve requests at runtime and make sure they end up at the right place.

I didn’t want to build a standalone API gateway and I didn’t want you to have to select and install one for this book. Instead I wrote a tiny bit of “proxy code” into the Home Service. And here’s how I did it.

Tip

The source code for the standalone Home Service client can be found in the associated GitHub repo. A running version of the client app described in this section can be found online.

First, I hardcoded some addresses for both a local version of the microservices and a remote running version of them:

// services
var addr = {};
addr.local = {};
addr.remote = {};
addr.selected = {};
addr.local.taskURL = "//localhost:8182/task/";
addr.local.userURL = "//localhost:8183/user/";
addr.local.noteURL = "//localhost:8184/note/";
addr.remote.taskURL = "//rwcbook16.herokuapp.com/task/";
addr.remote.userURL = "//rwcbook17.herokuapp.com/user/";
addr.remote.noteURL = "//rwcbook18.herokuapp.com/note/";

Next, near the top of the app.js for the Home Service, I added a bit of code that inspects the incoming request’s host header to see where that request is heading and use that to select a set of addresses:

// fix up redirects
if(root.indexOf("localhost")!==-1) {
  addr.selected = addr.local;
}
else {
  addr.selected = addr.remote;
}

Finally, whenever a consumer of the Home Service makes a call to one of the related services, I redirect that call to the proper standalone service component:

// task handler (send to external service)
try {
  if(flg===false && reTask.test(req.url)) {
    handleResponse(req, res, {code:302, doc:"",
      headers:{'location':addr.selected.taskURL}});
  }
}
catch(ex) {}

// user handler (send to external service)
try {
  if(flg===false && reUser.test(req.url)) {
    handleResponse(req, res, {code:302, doc:"",
      headers:{'location':addr.selected.userURL}});
  }
}
catch(ex) {}

// note handler (send to external service)
try {
  if(flg===false && reNote.test(req.url)) {
    flg = true;
    handleResponse(req, res, {code:302, doc:"",
      headers:{'location':addr.selected.noteURL}});
  }
}
catch(ex) {}

Now the client hosted by the Home service doesn’t need to know anything about where these other services are located. It just makes a call back to the Home service (e.g., /task/). Upon receiving the client request, the Home service selects the proper URL and passes that URL back to the client (via a 302 Redirect request). The client then uses this new URL to make a direct call to the running microservice.

In this way, the Home service does not handle the microservice requests, just the ones specific to the Home service. The remainder of the Home service is dedicated to serving up the HTML SPA container and associated JavaScript files for the client. We’ll look at that next.

The Multiformat Client SPA Container

The first thing to review is the HTML SPA container for our multiformat client. It looks similar to previous containers except for one thing. This container has bits of markup from the three previous containers all in one.

I’ll review the SPA container in a few key snippets. The first one is the top of the HTML <body> section. It contains a fixed menu area (1) that holds a set of relative URLs—one for each service. There is also a “shared layout” area (2) that holds the title, content, and error elements. The fixed elements are new for our clients, but the other sections should look familar.

<!DOCTYPE html>
<html>
  <body>
    <!-- fixed menu -->
    <div id="menu"> 1
      <div class="ui blue fixed top menu">
        <a href="/home/" rel="home" class="item" title="Home">Home</a>
        <a href="/task/" rel="task" class="item ext" title="Tasks">Tasks</a>
        <a href="/user/" rel="user" class="item ext" title="Users">Users</a>
        <a href="/note/" rel="note" class="item ext" title="Notes">Notes</a>
      </div>

      <!-- shared layout --> 2
      <h1 id="title" class="ui page header"></h1>
      <div id="content" style="margin: 1em"></div>
      <div id="error"></div>
    </div>
    ...

The next three markup blocks in the HTML container match up to the three media type formats this client understands. For example, here is the markup for handling Collection+JSON responses:

<!-- cj layout -->
<div id="cj" style="display:none;">
  <div id="c-links" style="display:none;"></div> 1
  <div style="margin: 5em 1em">
    <div class="ui mobile reversed two column stackable grid">
      <div class="column">
        <div id="c-items" class="ui segments"></div> 2
      </div>
      <div class="column">
        <div id="c-edit" class="ui green segment"></div> 3
        <div id="c-template" class="ui green segment"></div> 4
        <div id="queries-wrapper">
          <h1 class="ui dividing header">
            Queries
          </h1>
          <div id="c-queries"></div> 5
        </div>
      </div>
    </div>
  </div>
</div>

Note the callouts show Cj-specific elements for links (1), items (2), an edit block (3), the template (4), and queries (5).

The next markup block in the client is for rendering Siren responses where the callouts identify the siren links (1), properties (2), entities (3), and actions (4):

<!-- siren layout -->
<div id="siren" style="display:none;">
  <div id="s-links"></div> 1
  <div style="margin: 5em 1em">
    <div class="ui mobile reversed two column stackable grid">
      <div class="column">
        <div id="s-properties" class="ui segment"></div> 2
        <div id="s-entities" class="ui segments"></div> 3
      </div>
      <div class="column">
        <div id="s-actions"></div> 4
      </div>
    </div>
  </div>
</div>

Then there is the HAL section of markup which holds the links, embedded, and properties (1, 2 and 3, respectively). There is also an element to hold all the input forms (4) for HAL interactions:

<!-- hal layout -->
<div id="hal" style="display:none;">
  <div style="margin: 5em 1em">
    <div id="h-links" style="margin-bottom: 1em"></div> 1
    <div class="ui mobile reversed two column stackable grid">
      <div class="column">
        <div id="h-embedded" class="ui segments"></div> 2
        <div id="h-properties"></div> 3
      </div>
      <div class="column">
        <div id="h-form"></div> 4
      </div>
    </div>
  </div>
</div>
Tip

I actually could have created a single standardized SPA block of elements that all three of the message formats could use. But, for this sample, I wanted to make it easy to see how each message model maps to HTML blocks. In a robust production app, I’d probably use just one set of container elements—and even those might be dynamically generated at runtime based on the media type of the response.

Finally, at the bottom of the HTML page is a series of references to JavaScript files (1)—one for each format (cj-lib.js, siren-lib.js, and hal-lib.js) along with the local client files (2, dom-help.js and home.js). At 3, you can see the home client firing up and waiting for the next human interaction:

  <script src="cj-lib.js">//na </script> 1
  <script src="siren-lib.js">//na </script>
  <script src="hal-lib.js"?>//na </script>
  <script src="dom-help.js">//na </script>
  <script src="home.js">//na </script> 2
  <script>
    window.onload = function() { 3
      var pg = home();
      pg.init();
    }
  </script>

The Format-Switching Client UI

As you might be able to guess from the way the HTML looks for the client, this app is designed to handle three different media type responses: HAL, Siren, and Cj. The way this works is that libraries for each of them are loaded at runtime and then, as each request comes in, it is routed to the proper library and rendered in the appropriate HTML block. That functionality is contained in the home.js script.

The home.js script is not very big, and there are two parts worth reviewing here. First, at the start of the home.js script, all the other format libraries are initialized and the static page is filled in.

Here’s what that code looks like:

function home() {

  var d = domHelp();
  var cj = cjLib(); 1
  var siren = sirenLib();
  var hal = halLib();

  var global = {};
  global.accept = "application/vnd.hal+json,"  2
    + "application/vnd.siren+json,"
    + "application/vnd.collection+json";

  // init library and start
  function init() {
    cj.init(this.req, this.rsp, "TPS - Tasks"); 3
    siren.init(this.req, this.rsp, "TPS - Users");
    hal.init(this.req, this.rsp, "TPS - Notes");

    hideAll();
    setTitle();
    setHome();
    setLinks();
    setContent();
  }

In the preceding snippet you see where each library is loaded (1) and then intitialized (at 3). Note that pointers to this module’s Ajax object (this.req and this.rsp) are passed to each format library. That makes sure all requests originate from this module where they can be further inspected and then properly routed. You can also see (at 2) that the client’s HTTP ACCEPT variable is initialized to include all three of the formats this client understands when talking to services.

When up and running, the client code renders this menu and a bit of static content, then waits for user clicks (see Figure 9-1).

rwcl 0901
Figure 9-1. Rendering the home screen in the multiclient

Another important element in this snippet is at 2. This line of code sets the default accept header for our multiformat client by loading it with all three formats this client understands. Now, each initial request to any of the services will have the following value for the accept header:

Accept: application/vnd.hal+json, application/vnd.siren+json,
  application/vnd.collection+json

This is the HTTP way for a client to tell a server:

“Hey service, I understand these three formats. Please send your responses as either a HAL, Siren, or Cj message. kthxbye.”

It is up to the server to read and honor this request.

The other part of this default request is handling the response and routing it to the right library. In the home.js client code, the routing between libraries happens in the HTTP request handler. There, the library inspects the content-type header and routes the incoming response accordingly:

function rsp(ajax) {
  var ctype

  if(ajax.readyState===4) {
    hideAll();
    try {
      ctype = ajax.getResponseHeader("content-type").toLowerCase();
      switch(ctype) {
        case "application/vnd.collection+json":
          cj.parse(JSON.parse(ajax.responseText));
          break;
        case "application/vnd.siren+json":
          siren.parse(JSON.parse(ajax.responseText));
          break;
        case "application/vnd.hal+json":
          hal.parse(JSON.parse(ajax.responseText));
          break;
        default:
          dump(ajax.responseText);
          break;
      }
    }
    catch(ex) {
      alert(ex);
    }
  }
}

The code just shown looks very similar to the representor.js code we’ve seen in the server-side implementations used throughout the book (see “Implementing the Message Translator Pattern”). This client code is actually the mirror to that server code. That’s because this code is an implementation of a client-side representor. It takes the public representation (in HAL, Siren, or Cj) and converts that into the client’s base object graph, which is, for human-centric web browsers, the HTML DOM. The important difference is that the server-side representor uses the client’s accept header to route the internal object graph to the proper representor. And the client-side representor (seen previously) uses the server’s content-type header to route the external representation to the proper library to convert that message into a local HTML DOM for display.

Tip

This is an important design pattern for implementing loosely coupled interactions between components on a network. They share a common structured message model (HAL, Siren, Cj) and send them back and forth using a clear metadata tag to help receivers identify the message model. It is up to each party (providers and consumers) to do the work of translating between their own internal object models and the format-based shared message models.

Once the routing is decided, the entire response is passed to that library’s parsing routine to handle all the translating and rendering. So, let’s take a quick look at the media-type libraries for this client.

The Cj Render Library

In the following Collection+JSON render library (cj-lib.js) snippet, you can see the init() and parse() routines. At 1 the init routine accepts the pointers to the shared ajax handlers. Once the rsp handler in home.js receives the server response and routes it back here (via the parse method), the incoming Cj message is stored (2) and the response is rendered (3) just as in the dedicated Cj client. After all the rendering is done, the <div id="cj>…</div> block is revealed (4) to wait for human interaction. It’s the classic Request, Parse, Wait (RPW) interaction pattern from Chapter 5 in action:

// init library and start
function init(req,rsp) { 1
  global.req = req;
  global.rsp = rsp;
}

// primary loop
function parse(collection) {
  var elm;

  // store response
  global.cj = collection; 2

  //render 3
  dump();
  title();
  content();
  items();
  queries();
  template();
  error();
  cjClearEdit();

  // show
  elm = domHelp.find("cj"); 4
  if(elm) {
    elm.style.display="block";
  }
}

Figure 9-2 shows what the actual HTML UI looks like when handling Cj responses from the Task Service.

rwcl 0902
Figure 9-2. Rendering Cj responses in the multiclient

The Siren render library

The Siren parse routine looks similar (including the “reveal” code at 1). You’ll notice the title is passed in from the home.js routine. This is an optional item that covers the lack of a built-in title element in Siren. When using differing response formats in the same UI, there will naturally be some variances in the UI details, so adding these optional elements helps:

// init library and start
function init(req, rsp, title) {
  global.req = req;
  global.rsp = rsp;
  global.title = title||"Siren Client";
}

// primary loop
function parse(sirenMsg) {
  var elm;

  global.sirenMsg = sirenMsg;

  sirenClear();
  title();
  getContent();
  dump();
  links();
  entities();
  properties();
  actions();

  elm = domHelp.find("siren"); 1
  if(elm) {
    elm.style.display="block";
  }
}

Figure 9-3 shows the same client rendering Siren responses from the User Service.

rwcl 0903
Figure 9-3. Rendering Siren responses in the multiclient

The HAL render library

And finally, the HAL parse routine (see the following code). By now, it should all seem rather straightforward. The pattern is simple: initialize the library with request and response pointers; when a request is routed into the library, render it as you usually do and then reveal the results for the human to deal with.

Here’s what the HAL initial code looks like:

// HAL init library and start
function init(req, rsp, title) {
  global.title = title||"HAL Client";
  global.req = req;
  global.rsp = rsp;
}

// primary loop
function parse(hal) {

  global.hal = hal;

  halClear();
  title();
  setContext();
  if(g.context!=="") {
    selectLinks("list", "h-links");
    content();
    embedded();
    properties();
  }
  else {
    alert("Unknown Context, can't continue");
  }
  dump();

  elm = domHelp.find("hal");
  if(elm) {
    elm.style.display="block";
  }
}

Figure 9-4 shows the rendering of HAL responses from the Note Service.

Again, the exact details of the UI vary depending on which response format is returned. This is natural. Just as we get varying UI displays when moving from one website to another with our default client (the web browser), we’ll likely experience the same varying UI experience when we move from one microservice to another while using our multilingual client.

An Experiment for the Reader

The microservices I implemented are actually able to deliver their functionality in more than one representation format. For example, the Task Service can speak HAL and Siren just as well as Cj. What do you think would happen to the multiformat client if you simply change the response formats of the microservices at runtime? If the client and microservers are coded properly, the apps would function just fine (although there’d be minor UI experience differences).

rwcl 0904
Figure 9-4. Rendering HAL responses in the multiclient

Let’s wrap this up with a short summary of what we learned in this chapter.

Summary

After all the work creating dedicated UI clients for each of three selected hypermedia formats (Chapter 4, HAL Clients, Chapter 6, Siren Clients, Chapter 8, Collection+JSON Clients), this chapter brought that all together in a single user interface experience. We now have a single API client application that can talk to services in one of three message formats. And to make it all possible, we decomposed our TPS web API into three standalone, single-capability microservices, each making their own decision on which format to use for responses. We saw that—with just minor changes in our standalone media type libraries—we were able to use previous code libraries as plug-ins for our single multilingual client app:

Handling multiple formats

It should be clear that adding support for another media type (e.g., Atom, UBER, Mason, etc.) is not at all that daunting. For clients, we just need to do the same work of building up a working client-side rendering library and add it as another plug-in for this client. For servers, we need to implement the patterns covered in Chapter 3, The Representor Pattern. Then both client and server can use HTTP metadata (accept and content-type headers) to trigger translations from shared messages to internal object models.

Adapting to changes via metadata

One of the things we were able to learn from these experiments is that, in order for clients to successfully adapt to changes on the server, two things must be present. First, servers must promise to only introduce backward-compatible changes—ones that will not invalidate existing implementations. Second, clients must be able to acquire their knowledge of Objects, Addresses, and Actions from metadata in messages—not from source code. It is the power of recognizing and processing metadata in responses that provides clients with the ability to adapt.

Supporting backward compatibility

We also saw that both service providers and API consumers were coded to encourage backward-compatibility and reduce the occurrence of breaking changes by applying the rules we covered in Chapter 7, Versioning and the Web.

Leveraging interaction design

The examples in the book all focused on implementing the RPW loop for human-centric client applications. As mentioned in Chapter 5, The Challenge of Reusable Client Apps, our hypermedia clients were designed to implement a version of Verplank’s DO-FEEL-KNOW loop. They rely on humans to handle the FEEL (sense) and KNOW (decide) elements while the machine (the client app) handles the DO step. Building clients that can also handle FEEL and KNOW steps will require implementing some level of additional understanding into the client app—something that is certainly possible but outside the scope of this book.

In some ways, we’ve come full circle. We pointed out in Chapter 1, Our HTML Roots and Simple Web APIs that the initial power of the web browser was that it was able to connect to any web server, anywhere with any additional editing or alteration. That’s because the web browser relied on the HTML hypermedia format to understand the interaction design of the web server. The HTML browser does very well on the OAA Challenge.

However, as web-based APIs became commonplace, developers ended up creating an interaction design that weakened the ability of clients to adapt to changes in Objects, Addresses, and Actions resulting in a reliance on the kinds of JSON-only clients we saw at the start of the book. These clients soundly failed the OAA Challenge!

But, as we saw over the last several chapters, there are now a handful of powerful structured message formats (HAL, Siren, Cj, and others) that allow developers to re-capture the ability to build adaptable clients—ones that do well on the OAA Challenge. And that can lead to more freedom and innovation for services, too.

Hopefully, this last chapter has given you some ideas of your own on how you can use media-type libraries as plug-ins to improve the flexibility of your own API clients and how you can use hypermedia to improve their adaptability over time. Finally, you may now have some ideas on how you can start creating machine-to-machine clients that can create their own specifications for solving a problem and interpreting responses at a more meaningful level. Then, you’d have API consumers that can go further and last longer without the need for direct human interaction at every click and turn along the way.

But that’s an adventure for another time.

References

  1. Fowler maintains a landing page for the microservices topic at his public blog.

  2. Adrian Cockcroft’s slides and a video of his “State of the Art in Microservices” is a great presention worth checking out.

  3. Read more about Eric Evans and DDD on the Domain Language website. Fowler also has a page about Bounded Context.

  4. You can read a PDF scan of the original foreword to the 1978 UNIX Timesharing System online.

  5. The book Microservice Architecture (O’Reilly) is a solid introduction to the general topic of microservices in an organizational context.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.117.35