Writing a Hypermedia API client in Ruby

Ismael Celis
Ruby Hypermedia Client in terminal session
A Ruby Hypermedia client interacting with a REST API in a terminal session.

Much ink and pixels have been spent discussing the virtues and flaws of Hypermedia for API design. Like with REST, the sheer amount of theory and jargon around the subject can make it hard to understand the potential benefits for you as an API developer, the cost of implementation and the consequences it would have on the way you build, and interact with REST APIs.

In this article I’ll show the outlines of what a Hypermedia API looks like, and how a generic Ruby client for such APIs can be designed following these patterns.

Our definition of Hypermedia will be this: Hypermedia for API design means adding links to your API responses.

By link, I mean a link as you already know it from HTML documents:

<a href="http://server.com/foobar">Foo bar</a>

<link rel="stylesheet" href="/styles.css" />

So at the very least, a link is an address or pointer to a separate resource somewhere in the network.

But, in our definition, this will also constitute a link:

<form action="/orders/place" method="PUT">
  <input type="text" name="discount_code" value="" />
  <select name="shipping">
    <option value="1">Free shipping</option>
    <option value="2">Expensive shipping</option>
  </select>
</form>

So not just an address, but also the minimum information needed to instruct the client on how it should interact with the referenced resources. In the example above, a HTML form tells the client —the browser— that it must send a PUT request, and also defines a schema for the data expected by the server.

In other words, links or forms encapsulate actions that the client may take over resources, or ways in which it can change the state of a resource. These, in a nutshell, are the ideas behind HATEOAS, and they have been derived from the way the World Wide Web works as we know it and interact with every day.

An example

So how does any of this apply to REST APIs?

Let’s imagine a simple shopping cart API that shows us information about an order.

GET /orders/123

{
  "updated_at": "2017-04-10T18:30Z",
  "id": 123,
  "status": "open",
  "total": 1000
}

Now let’s say that you can place an open order by issuing a PUT request against a resource, something like

PUT /orders/123/completion

The server updates the state of the order and returns a new representation:

{
  "updated_at": "2017-04-10T20:00Z",
  "id": 123,
  "status": "placed",
  "total": 1000
}

Nothing new here. This is how most of us build REST/JSON APIs.

How do you know that you can place an open order? You’d check the relevant documentation, which would include information on the URL to use, the required request method, available parameters, etc.

If you wanted to use this API in Ruby, you’d grab any number of available HTTP client libraries and end up with something like this:

# get an order
order = Client.get("/orders/123")

# place the order
order = Client.put("/orders/123/completion")

After a while, manually concatenating URL paths gets tedious and error-prone, so as an API client author you’ll probably come up with something slightly more domain-specific:

order = Client.get_order(123)

order = Client.place_order(123)

This is what many Ruby API client libraries look like.

Now let’s re-implement the same example using a Hypermedia approach. State transitions —actions— can be encoded as links in the API responses themselves.

GET /orders/123

{
  "_links": {
    "place_order": {
      "method": "put",
      "href": "https://api.com/orders/123/completion"
    }
  },
  "updated_at": "2017-04-10T18:30Z",
  "id": 123,
  "status": "open",
  "total": 1000
}

I’m using an extension of the HAL specification to encode a “place_order” link, including its request method and target URL.

For one, this makes the order resource a little bit more informative to humans: not only does it tell you about the current state of an order, but what you can do with it.

The API client

As an API client implementor, you can leverage this simple convention and write a generic client that learns what actions are available from the API itself. This is particularly easy to do in Ruby.

Let’s start from the top. First thing is to implement a class that wraps any API resource and exposes its attributes and links. I’m calling it Entity.

class Entity
  def initialize(data, http_client)
    @data = data
    @links = @data.fetch("_links", {})
    @client = http_client
  end

  # ...
end

data is the JSON data (a Hash) for the resource itself. http_client for now is anything that supports the basic HTTP operations (get, post, put, delete, etc). It can be an instance of Faraday or anything, really.

We then use the method_missing and respond_to_missing? combo to delegate data access to any properties available in the resource data.

class Entity
  # ...

  def method_missing(method_name, *args, &block)
    if @data.key?(method_name)
      @data[method_name]
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @data.key?(method_name)
  end
end

With this, we can access regular properties.

order = Entity.new(json_data, http_client)

order.id # 123
order.status # "open"

What about links? We can delegate those to a Link class that’ll handle making the relevant HTTP request and returning the response.

class Entity
  # ...

  def method_missing(method_name, *args, &block)
    if @data.key?(method_name) # if it's a regular property...
      @data[method_name]
    elsif @links.key?(method_name) # if it's a link...
      Link.new(@links[method_name], @client).run(*args)
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @data.key?(method_name) || @links.key?(method_name)
  end
end

We instantiate a Link passing the link data and the HTTP client instance, and then we run it with any optional arguments.

This allows us to run those links like any regular method call. For example placing an order:

order = Entity.new(json_data, http_client)

# this will issue a PUT request under the hood
placed_order = order.place_order

The link wrapper class delegates the actual HTTP request handling to the underlying HTTP client.

link = Link.new({
  "href": "https://api.com/orders/123/completion",
  "method": "put"
}, http_client)

We initialize it with the link data and the HTTP client.

class Link
  attr_reader :request_method, :href

  def initialize(attrs, http_client)
    @request_method = attrs.fetch("method", :get).to_sym
    @href = attrs.fetch("href")
    @http_client = http_client
  end
  # ...
end

The run method uses the HTTP client to issue the appropriate HTTP request and wraps the response in a new Entity.

class Link
  # ...

  # we'll assume that request bodies need to be JSON-encoded
  def run(payload = {})
    response = case request_method
      when :get
        @http_client.get(href, payload)
      when :put
        @http_client.put(href, JSON.dump(payload))
      when :delete
        # etc
    end

    # error handling on failed responses...

    # wrap response body in new Entity
    Entity.new(response.body, @http_client)
  end
end

Because a Link sends optional parameters to the relevant endpoint, we can pass any data expected by the API. For example:

placed_order = order.place_order(
  discount_code: "ABC"
)

The presence or absence of links in a resource is meaningful information. For example, an “open” order may include a place_order link, but an order that has already been placed shouldn’t let the client place it again.

We can add some syntax sugar to our Entity class to better interrogate resources for their supported links.

class Entity
  # ...
  def can?(link_name)
    @links.key? link_name.to_s
  end
end

So client code can decide what to do based not so much on the data in a resource, but on its capabilities.

if order.can?(:place_order)
  # PUT https://api.com/orders/123/place
  order = order.place_order
end

if order.can?(:start_order_fulfillment)
  # POST https://api.com/orders/123/fulfillments
  fulfillment = order.start_order_fulfillment(...)
end

This pattern has the side-effect of encouraging you to keep the business logic on the server side. The client app is driven by the API’s capabilities, presented dynamically to the client along with every resource.

For example, instead of hardcoding conditional logic to decide whether of not to show a button to place an order (based on the current state of the order, possibly), a client application can blindly render the relevant UI elements if the expected links are present in the current resource.

<% if order.can?(:place_order) %>
  <button type="submit">Place this order</button>
<% end %>

A client written in this way can be kept pretty generic. New endpoints available in the API don’t need client implementors to release new versions of the client library. As long as the API presents a new link, the client can follow it. The release cycles of API and client are thus disentangled.

The API root

The API root is the entry point into a Hypermedia-enabled API, and the only URL a client needs to know about. Much as a website’s homepage, the root resource will most likely include a list of links to the main operations supported by your API.

GET /

{
  "_links": {
    "orders": {
      "href": "https://...",
    },
    "create_order": {
      "href": "https://...",
      "method": "post"
    }
  }
}

We only need to wrap up our Entity and Link classes into a client that we can initialize with a root URL.

class ApiClient
  def initialize(root_url, http_client)
    @root_url, @http_client = root_url, http_client
  end

  def root(url = @root_url)
    response = @http_client.get(url)
    Entity.new(response.body, @http_client)
  end
end

Once on the root resource, a client can just follow available links by name. The actual URLs they point to may or may not be on the same host. As long as the responses conform to the same conventions, the client —and the users of your SDKs— don’t need to care.

api = ApiClient.new("https://api.com", SomeHttpClient)
root = api.root

# create order
order = root.create_order(line_items: [...])

# add items
order.add_line_item(id: "iphone", quantity: 2)

# place it
order = order.place_order

Workflows

As an API designer, I’ve found that this approach encourages me to not only think of individual endpoints but of entire workflows through the service. How do you start and place an order? How do you upload an image? By adding the right links in the right places I can help make it easier for my customers to accomplish particular tasks.

These workflows can form the core of the API’s documentation, too, by documenting the sequence of links to be run for each use case instead of just singular endpoints.

Pagination

A nice workflow to implement in this way is paginating over resources that represent lists of things.

GET /orders

{
  "_links": {
    "next": {
      "href": "https://api.com/orders?page=2"
     }
   },
   "total_items": 323,
   "items": [
      {"id": 123, "total": 100},
      {"id": 234, "total": 50},
      // ... etc
   ]
}

In this convention, a list resource will have an items array of orders. If there are more pages available, the resource can include a next link.

We can extend Entity to implement the Enumerable interface for list resources.

module EnumerableEntity
  include Enumerable

  def each(&block)
    self.items.each &block
  end
end

class Entity
  def initialize(data, client)
    # ...
    self.extend EnumerableEntity if data.key?("items")
  end
end

The client can now ask the entity whether it can be paginated.

page = root.orders

page.each do |order|
  puts order.status
end

page = page.next if page.can?(:next)

We can take this one step further and implement an Enumerator that will consume the entire data set in a memory-efficient way.

module EnumerableEntity
  # ...
  def to_enum
    # start with the first page
    page = self

    Enumerator.new do |yielder|
      loop do
        # iterate over items in the first page
        page.each{|item| yielder.yield item }
        # stop iteration if we've reached the last page
        raise StopIteration unless page.can?(:next)
        # navigate to the next page and iterate again
        page = page.next
      end
    end
  end
end

We can now treat potentially thousands of orders in a paginated API as a simple array.

all_orders = root.orders(sort: "total.desc").to_enum

all_orders.each{|o| ...}

all_orders.map(&:total)

all_orders.find_all{|o| o.total > 200 }

Non-hypermedia APIs

Even if your API doesn’t include links, a generic Ruby client like the one described here can be used to describe endpoints in your service with little extra work.

class ApiClient
  # ...

  def from_hash(data)
    Entity.new(data, @http_client)
  end
end
api = ApiClient.new(nil, SomeHttpClient)
orders_service = api.from_hash({
  "_links": {
    "orders": {"href": "...", "method": "get"},
    "create_order": {"href": "...", "method": "post"}
  }
})

order = orders_service.create_order(...)
order = order.place_order
# etc

New releases of such a client could consist of just a YAML file with the entire API definition.

Reference