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
Live slides
http://cass.ee/pyconsette1
Code
https://github.com/jcassee/registronavale
API browser
https://browser.registronavale.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
- The URI of the root resource
- A set of relations
- A set of profiles
"It's not an API, it's a protocol!"
Hypermedia clients ...
- know about profiles, relations 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
Relish
Featurebook
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,136