How to Take Your API From RPC to Hypermedia in 7 Steps

Continued from page 5. 

For example, let’s create a new type in our RAML specification called Operation and let’s allow Links to have an associated array of operations.

We will also introduce two ‘template’ types UserTemplate and TodoTemplate to document the shape of the payload required by the service to create a new User and Todo respectively:

#%RAML 1.0

title: Todos Service
baseUri: http://todosapp.com/api/version_4
mediaType: [ application/ld+json, application/json, application/xml ]

types:
  URI: string
  Type: string
  Operation:
    method:
      enum: [ GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS ]
    expects: Type
    returns: Type
  Link:
    properties:
      @id: URI
      operations: Operation[]
  Context:
    properties:
      @vocab: URI
  EntryPoint:
    @context: Context
    @id: URI
    users: Link
  UserTemplate:
    properties:
      name?: string
      email: string
  User:
    properties:
      @context: Context
      @id: URI
      email: string
      name: string
      todos: Link
  Todo:
    properties:
      @context: Context
      @id: URI
      title: string
      description: string
      user: Link
  TodoTemplate:
    properties:
      title: string
      description?: string
  Collection:
    properties:
      @context: Context
      @id: URI
      members: Link[]

/:
  get:
    description: Entry point for the application
    responses:
      200:
        body: EntryPoint

Now, if we request any resource in the API we will retrieve a much richer set of hypermedia controls with enough control information to consume the whole HTTP uniform interface.

{
  "@context": { "@vocab": "http://todosapp.com/api/vocab#" },
  "@id": "http://todosapp.com/api/version_4/users/1#self",
  "name": "John Doo",
  "email": "john.doo@test.com",
  "todos": {
    "@id": "http://todosapp.com/api/version_4/todos#self",
    "operations": [
      {
        "method": "GET",
        "returns": "Collection"
      },
      {
        "method": "POST",
        "expects": "TodoTemplate",
        "returns": "Todo"
      }
    ]
}

Our data graph has also been enriched with additional nodes for the hyper-media controls. The control nodes are in this particular implementation anonymous, blank nodes in the graph, with URIs that cannot be dereferenced outside the local graph.

Consuming this version of the API, a generic hypermedia client could query the data graph encoded in the resource representation using SPARQL to discover the hypermedia controls embedded in the resource representation:

SELECT ?resource ?method
WHERE {
  ?resource <http://todosapp.com/api/vocab#oerations> ?operation .
  ?operation <http://todosapp.com/api/vocab#method> ?method .
}
ORDER BY(?resource)

To obtain the results:

|   ?resource                                     |  ?method   |
|-------------------------------------------------|------------|
| <http://todosapp.com/api/version_4/todos#self>  | GET        |
| <http://todosapp.com/api/version_4/todos#self>  | POST       |
|-------------------------------------------------|------------|

API Version 5: Meta-data

Two of the major goals of REST are robustness and extensibility: the ability to gradually deploy changes in the architecture.

This is granted by the extensible elements of the HTTP protocol, like versioning or headers and by separating the parsing of the messages from their semantic. HTTP also constrains these messages to be self-descriptive by the inclusion of meta-data in the resource representation.

In the Web of documents the notion of self-descriptive messages is usually equated to assigning media types to the messages. This is usually just a way of describing the format of the message for the client to be able to select a supported representation.

When we are working with APIs, meta-data we should also try to make our messages, the returned representation of a resource, to be self-descriptive, introducing as much meta-data as it is required for a client to automatically process the representation.

In a narrow sense this conception of meta-data matches the notion of schema: “the payload is a map with two properties (name and email) and types ‘string’ and ‘string'”. It can also mean the notion of a ‘type’ for the resource: “User”, “Todo”.  Ultimately should mean the semantics of the resource: “name and email of the persons using our service”.

In the version 4 of the API these meta-data are encoded in our RAML specification. The main problem with this approach is that this information is not encoded in the resource representations at all. So far, it lives in a document completely outside of the API and it is not available for API clients.

In this version of the API we are going to make our API representation self-descriptive using hypermedia. First, we are going to introduce a property resource_type designating the type of resource for the different resources. Secondly, we will add a hyper-link linking to the RAML description of the API:

#%RAML 1.0

title: Todos Service
baseUri: http://todosapp.com/api/version_5
mediaType: [ application/ld+json, application/json, application/xml ]

types:
  URI: string
  Type: string
  Operation:
    method:
      enum: [ GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS ]
    expects: Type
    returns: Type
  Link:
    properties:
      @id: URI
      operations: Operation[]
  Context:
    properties:
      @vocab: URI
  EntryPoint:
    @context: Context
    @id: URI
    described_by: Link
    resource_type: Type
    users: Link
  UserTemplate:
    properties:
      name: string
      email?: string
  User:
    properties:
      @context: Context
      @id: URI
      described_by: Link
      resource_type: Type
      email: string
      name: string
      todos: Link
  Todo:
    properties:
      @context: Context
      @id: URI
      described_by: Link
      resource_type: Type
      title: string
      description: string
      user: Link
  TodoTemplate:
    properties:
      title: string
      description?: string
  Collection:
    properties:
      @context: Context
      @id: URI
      described_by: Link
      resource_type: Type
      members: Link[]

/:
  get:
    description: Entry point for the application
    responses:
      200:
        body: EntryPoint

Now if we request any resource of the API, the payload we will include the additional meta-data:

{
  "@context": { "@vocab": "http://todosapp.com/api/vocab#" },
  "@id": "http://todosapp.com/api/version_5/users/1#self",
  "resource_type": "User",
  "name": "John Doo",
  "email": "john.doo@test.com",
  "described_by": { "@id": "http://todosapp.com/api/version_5/spec.raml" },
  "todos": {
    "@id": "http://todosapp.com/api/version_4/todos#self",
    "operations": [
      {
        "method": "GET",
        "returns": "Collection"
      },
      {
        "method": "POST",
        "expects": "TodoTemplate",
        "returns": "Todo"
      }
    ]
}

And the additional nodes will be added to the resource data graph:

This solution has a number of shortcomings:

  • Some meta-data is encoded as resources with URIs inside the data graph (like hyper-media controls) other is encoded as RAML in a document linked through a URL
  • The connection is made by a plain string, the type name in the RAML document, it is impossible to link it using a hyper-link
  • Clients need two different parsers, one for JSON-LD and another one for RAML
  • The protocol to discover meta-data is completely private, the client needs to understand the http://todosapp.com/api/vocab#described_by property
  • The size of the payloads keep on increasing with more meta-data

API Version 6: Self-descriptive API

A possible solution for the issues we have found in version 5 of the API, that is consistent with our design, is to move the meta-data from the representations to a different plane above the representations and link the meta-data to the resources using a hyper-link.

Antonio Garrote Principal Engineer at Mulesoft in the mornings, researching on RESTful semantic web services in the evenings. Clojure enthusiast all day long.

Comments