Hypermedia Services and MVC
This blog post is in response to the discussion started by the tweet below:
Incidentally, MVC is a REALLY poor fit for designing hypermedia services. So that's fun, everyone who thought you knew what you were doing.
— Nate Abele (@nateabele) January 3, 2014
This post is not intended to present the best way to design and build hypermedia services. My goal is present a high level description of how we have built and designed a hypermedia API at HauteLook within the context of the MVC Architectual Pattern. That being said, I am always looking for ways to improve.
Let's start with the controller. According to the Gang of Four, the controller "defines the way the user interface reacts to user input". In the context of a hypermedia service, user input comes in the form of a request. The request is most commonly sent using HTTP, but could use another protocol such as FTP. We will be assuming HTTP for the rest of this post. The user interface is the API response the server sends back to the client. In a hypermedia API, the response is based on which resources the client (user) is requesting and what media type the client prefers.
Let us use the example below. We can assume that the client is knowledgeable of the /users/42
URL because it made a HTTP GET request to /
and received a list of user relations it can then request.
GET /users/42 HTTP\1.1
Host: example.com
Accept: application/hal+json
The controller will first fetch the user resource. If successful, the controller will specify a 200 response code, any necessary headers and then pass that resource to the view along with the desired client's desired media type. However, there are many reasons why the request may not be successful. The resource /users/42
may not exist, the client may not be authorized to make the request, pre-conditions of the request may not be satisifed or any other number of problems. In any of those error cases, the controller will issue the proper response code, headers and any body necessary to represent the current state to the client.
The resources in our hypermedia API are models. We have some code samples of models for address and user resources below. I chose ruby as it makes the code very concise. How exactly these models are populated is an exercise left up to the reader. What we do not want to do is simply transfer the data in our persistence layer (i.e. database) directly to the client. If we are using something like a relational database, there may be many tables required to accurately represent one resource. Take a look at the Address
model below. We may need to execute a SQL statement that joins some hypothetical addresses
, states
and countries
tables together in order to create address resources. Our goal is to encapsulate the resources you want to represent to the client. If the underlying persistence layer changes, the resource should not change.
class Address
attr_accessor :line1, :line2, :state, :country,
:postal_code
end
class User
attr_accessor :user_id, :email, :first_name, :last_name,
:addresses
def initialize(addresses)
@addresses = addresses
end
def fullName()
"#{first_name} #{last_name}"
end
end
Once we have our resources created, we need to represent them to the client using the view. The view is only concerned with how we are presenting our resource to the client. It is not scalable to explicitly write out every resource/media-type combination. We want to describe how to represent a resource in an agnostic way. We then feed those descriptions into a serializer that is aware of HAL, Atom, JSON-API, etc and can generate the output based on the desired media type of the client.
Here is an example DSL that describes how to represent a user resource to the client:
relations:
self:
href:
route: /users/:user_id
params:
user_id: id
/rels/orders:
href:
route: /orders?user_id=:user_id
params:
user_id: id
/rels/addresses:
href:
route: /users/:user_id/addresses
params:
user_id: id
embed:
property: addresses
properties:
email: email
name: fullName
The user class is serialized into the correct media type based on the description. The relations
section describes how the user relates to other resources in the API. The properties
section describes how to show the resource to the client. In this case, we do not show first and last name separate properties but instead show one name
property. More importantly, we do not include the user_id in the response. It is probably not important to the client what the user id is, except to create URLs. We have the relations to avoid client-side URL generation though. Also, notice how the addresses are embedded in the representation of a user. The serializer would look up how to represent the address and serialize it accordingly.
The above DSL is loosely based on the Symfony 2 HATEOAS bundle that we use at HauteLook. Reading through the documentation you may notice that it uses funky PHP annotations. This is just a preference of the maintainers. There is also built-in support to use separate PHP or XML files to describe the view.
This architecture has made it fairly straight-forward to design and maintain a hypermedia API. The risk of changes at persistent layer leaking into the client representations is low. The models encapsulate the resources and we can make them as resistant to change as we want. The view presents the representation of the resource and how the resource relates to other resources to the client.