Developing a developer-friendly hypermedia API

Joost Cassee

PyCon Sette, 15 April 2016

with Django REST Framework and Behave

Painless Software

django-analytical
 

django-geckoboard
 

django-localeurl
 

angular-hypermedia

joost@cassee.net

@jcassee

jcassee.com

I will talk about

  • designing a hypermedia API with link relations and resource profiles.
     
  • implementing a hypermedia API in Django REST Framework
     
  • using Behave to both test and document the API.
     
  • safely breaking backward compatibility by using link deprecation.

Resource

URI

http://example.com/ship/9334026

"An entity"

Representation

{
  "name": "VOS PROMINENCE",
  "imo": 9334026,
  "type": "cargo"
}

Media Type

application/json

Type

Profile
http://example.com/profiles/ship

"It is a ship"

The resource identified by http://example.com/ship/9334026

{
  "name": "VOS PROMINENCE",
  "imo": 9334026,
  "type": "cargo"
}

and is represented as application/json by

has profile http://example.com/profiles/ship

Representational State

... Transfer

A resource identified by a http/https URL

can be manipulated using HTTP methods

GET

PUT

DELETE

PATCH

POST

Retreive a representation

Store a representation

Delete a resource

(Partially) update a resource

"Process" a representation

REST is CRUD

Expose business logic as state manipulation

  rocess

P

Hypertext reference

href

http://example.com/company/3342

"That entity over there"

References in resource state

{
  "name": "VOS PROMINENCE",
  "imo": 9334026,
  "type": "cargo",
  "ownerHref": "http://example.com/company/3342"
}

"That is the owner"

(semantics described by the profile)

Linking

Relation
http://example.com/relations/owner

"It has an owner"

The resource identified by http://example.com/ship/9334026

to the resource identified by http://example.com/company/3342

has relation http://example.com/relations/owner

HAL

Hypermedia Application Language

application/hal+json

{
  "name": "VOS PROMINENCE",
  "imo": 9334026,
  "type": "cargo",
  ...
{
  "name": "VOS PROMINENCE",
  "imo": 9334026,
  "type": "cargo",
  "_links": {
    "self": {
      "href": "http://example.com/ship/9334026"
    },
    "profile": {
      "href": "http://example.com/profiles/ship"
    },
    "http://example.com/relations/owner": {
      "href": "http://example.com/company/3342"
    }
  },
  ...
{
  ...
  "_embedded": {
    "http://example.com/relations/owner": {
      "name": "Vroon B.V.",
      "_links": {
        "self": {
          "href": "http://example.com/company/3342"
        },
        "profile": {
          "href": "http://example.com/profiles/company"
        }
      }
    }
  }
}

State reference

  • Part of resource state
  • Defined by profile
  • Mutable by client
  • Removal is backward-incompatible

Link relation

  • Part of API wiring
  • Defined by relation
  • Not mutable by client
  • Can be deprecated and removed

vs.

{
  "_links": {
    "rel": { "href": "uri" }
  }
}
{
  "property": "uri"
}

HATEOAS

Hypertext As The Engine Of Application State

"Use links to traverse the API"

Elements of a hypermedia API

  1. The URI of the root resource
  2. A set of relations
  3. A set of profiles

"It's not an API, it's a protocol!"

Hypermedia clients ...

  • know about profilesrelations and root URI
     
  • treat URIs as opaque
     
  • assume backward compatibility of state
     
  • let presence of links drive interactions
    (recover gracefully from missing links)

Hypermedia servers ...

  • decide what links to provide, based on
    • business workflow
    • user roles
    • service availability
  • decide what resources to embed, based on
    • business workflow
    • client-server negotiation

Hypermedia APIs ...

give you

  • reuse of relations and profiles
  • seamless navigation across services
  • well-defined API evolution mechanism

require

  • a departure from URL-centric design
  • client robustness w.r.t. links
  • design of API wiring

Reuse of relations and profiles

{
  "_links": {
    "self": { "href": "http://example.com/users/john" },
    "profile": { "href": "http://profiles.example.com/person" }
  }
}
{
  "_links": {
    "self": { "href": "http://example.com/clients/4335/contact" },
    "profile": { "href": "http://profiles.example.com/person" }
  }
}

Navigation across services

{
  ...
  "_links": {
    "self": {
      "href": "http://example.com/ship/9334026"
    },
    "http://example.com/relations/owner": {
      "href": "http://companies.example.com/3342"
    }
  }
}

API evolution (1) - topology change

{
"_links": {
  "http://example.com/relations/owner": {
    "href": "http://example.com/company/3342"
  }
}
{
"_links": {
  "http://example.com/relations/owner": {
    "href": "http://companies.example.com/3342"
  }
}

API evolution (2) - concept change

{
  "_links" : {
    "http://example.com/relations/owner" : {
      "href" : "http://example.com/company/3342",
      "deprecation": "http://example.com/docs/deprecated/owner.html"
    },
    "http://example.com/relations/ownership" : {
      "href" : "http://example.com/ship/9334026/ownership/3"
    }
  }
}

Django REST Framework

Models

domain entities and business rules

Serializers

representation structure

Renderers 

representation syntax

View(Set)s

resources

Mapping a hypermedia API

Media type 

use HalRenderer

Profiles

defined by the Serializer

Relations 

added by the View

Resources

are View(Set)s (using a Router)

Serializer

... using custom Router, ViewSets, Serializers
all working together: drf_hal module

Implementation

Let's take a look...

The problem with documentation

  • Examples and tutorials get out of sync
     
  • Reference documentation on hypermedia elements
    • relations
    • profiles

too finegrained

Living documentation

  • Test requirements
     
  • Document by example
     
  • Keep documentation up to date

Feature tests

Given

And

When

And

Then

a company named "Joost Shipping"

ship named "Providence" owned by "Joost Shipping"

you get the ship "Providence"

you follow the relation "ship-owner"

the response is ...

Getting information about companies that own ships

Get a ship's owner

Feature:

Scenario:

Gherkin language

Step implementation

  • a company named "{company}"
     
  • ship named "{ship}" owned by "{company}"
     
  • you get the ship "{ship}"
     
  • you follow the relation "{relation}"

behave & behave-django

Generate documentation

API evolution

Introduce ownership history

Company

Ship

Ownership

Company

Ship

{
  "_embedded": {
    "http://rels.registronavale.com/ship-owner": {
      "_links": {
        "self": {
          "href": "https://api.registronavale.com/company/1"
        }
{
  "_links": {
    "http://rels.registronavale.com/owner-history": {
      "href": "https://api-pr1.registronavale.com/ship-owners-history/12345"
    },
    "http://rels.registronavale.com/ship-owner": {
      "deprecation": "http://apidocs.registronavale.com/deprecation/...",
      "href": "https://api-pr1.registronavale.com/company/1"
    }

Do existing tests change?

No

  • check known state properties
  • robust with regard to linking or embedding
  • ignore deprecation

Yes

  • check complete representations
    (like this project does)
  • deprecated links disappear

I have talked about

  • designing a hypermedia API with link relations and resource profiles.
     
  • implementing a hypermedia API in Django REST Framework
     
  • using Behave to both test and document the API.
     
  • safely breaking backward compatibility by using link deprecation.

Thanks for listening!

Painless Software

Less pain, more fun

Developing a developer-friendly hypermedia API with django-rest-framework and behave

By Joost Cassee

Developing a developer-friendly hypermedia API with django-rest-framework and behave

Presented at PyCon Sette on 15 April 2016

  • 3,060