Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Designing Hypermedia APIs

Designing Hypermedia APIs

I gave this talk at TwilioCon 2012.

Steve Klabnik

October 17, 2012
Tweet

More Decks by Steve Klabnik

Other Decks in Programming

Transcript

  1. “People are fairly good at short-term design, and usually awful

    at long-term design. Most don’t think they need to design past the current release.” Problem Wednesday, October 17, 12
  2. “But what distinguishes the worst architect from the best of

    bees is this, that the architect raises his structure in imagination before he erects it in reality.” Karl Marx Wednesday, October 17, 12
  3. GET / HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: application/json Link: </profile>; rel="profile" { "statuses":[ { "body":"neato", "username":"steveklabnik" }, { "body":"testing testing", "username":"steveklabnik" } ], "template":{ "body":"", "username":"" }, "links":[ { "rel":"collection", "href":"/" } ] } Wednesday, October 17, 12
  4. GET / HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: application/json Link: </profile>; rel="profile" { "statuses":[ { "body":"neato", "username":"steveklabnik" }, { "body":"testing testing", "username":"steveklabnik" } ], "template":{ "body":"", "username":"" }, "links":[ { "rel":"collection", "href":"/" } ] } Wednesday, October 17, 12
  5. GET / HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: application/json Link: </profile>; rel="profile" { "statuses":[ { "body":"neato", "username":"steveklabnik" }, { "body":"testing testing", "username":"steveklabnik" } ], "template":{ "body":"", "username":"" }, "links":[ { "rel":"collection", "href":"/statuses" } ] } Wednesday, October 17, 12
  6. POST / HTTP/1.1 Host: www.example.com { "body":"hello, hypermedia", "username":"steveklabnik" }

    HTTP/1.1 201 Created Content-Length: 438 Content-Type: application/json Link: </profile>; rel="profile" Location: "/" {"message":"you are being redirected"} Wednesday, October 17, 12
  7. GET / HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: application/json Link: </profile>; rel="profile" { "statuses":[ { "body":"neato", "username":"steveklabnik" }, { "body":"testing testing", "username":"steveklabnik" }, { "body":"hello, hypermedia", "username":"steveklabnik" }, ], "template":{ "body":"", "username":"" }, "links":[ { "rel":"collection", "href":"/" } ]} Wednesday, October 17, 12
  8. Coupling - Demeter class Foo def initialize(bar) @bar = bar

    end def process @bar.qux.fetch_data end end Wednesday, October 17, 12
  9. Coupling - Demeter class Foo def initialize(bar) @bar = bar

    end def process @bar.qux.fetch_data end end Wednesday, October 17, 12
  10. Coupling - Demeter describe Foo do it “fetches data” do

    bar = Bar.new bar.stub(:qux => stub(:fetch_data => “data”) ) expect(Foo.new(bar).process).to eq(“data”) end end Wednesday, October 17, 12
  11. Coupling - Demeter describe Foo do it “fetches data” do

    bar = Bar.new bar.stub(:qux => stub(:fetch_data => “data”) ) expect(Foo.new(bar).process).to eq(“data”) end end Wednesday, October 17, 12
  12. Coupling - Demeter You can tell something is coupled because

    it breaks when you change it. Wednesday, October 17, 12
  13. Decoupling GET / HTTP/1.1 Host: www.example.com Accept: application/json HTTP/1.1 200

    OK Content-Length: 438 Content-Type: application/json Wednesday, October 17, 12
  14. Decoupling GET / HTTP/1.1 Host: www.example.com Accept: application/json HTTP/1.1 200

    OK Content-Length: 438 Content-Type: application/json Wednesday, October 17, 12
  15. Media Types Media type definition contains all information needed to

    implement a client and server for a given type. Wednesday, October 17, 12
  16. GET / HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: application/json Link: </profile>; rel="profile" { "statuses":[ { "body":"neato", "username":"steveklabnik" }, { "body":"testing testing", "username":"steveklabnik" }, { "body":"hello, hypermedia", "username":"steveklabnik" }, ], "template":{ "body":"", "username":"" }, "links":[ { "rel":"collection", "href":"/" } ]} Wednesday, October 17, 12
  17. GET /profile HTTP/1.1 Host: www.example.com HTTP/1.1 200 OK Content-Length: 438

    Content-Type: text/plain This server emits "microblogging JSON." ## Keys You can expect the following keys: statuses, template, links ## Link Relations You can expect a "collection" relation. GETing the URI will fetch the list of all statuses. POSTing a template to this URI will create a new status. Wednesday, October 17, 12
  18. Media Types def reply_to_tweet tweets = get_list_of_tweets reply_to = display_to_user(tweets)

    reply_data = display_require_form send_reply(reply_to, reply_data) end Wednesday, October 17, 12
  19. Media Types def reply_to_tweet tweets = get_list_of_tweets reply_to = display_to_user(tweets)

    reply_data = display_require_form send_reply(reply_to, reply_data) end Wednesday, October 17, 12
  20. Media Types Hypermedia Affordance <a href=”...” rel=”foo”>foo</a> <form action=”” method=”GET”>

    <input type=”text” name=”q” /> <input type=”submit” /> </form> Wednesday, October 17, 12
  21. Pagination The pagination info is included in the Link header.

    It is important to follow these Link header values instead of constructing your own URLs. In some instances, such as in the Commits API, pagination is based on SHA1 and not on page number. Link: <https://api.github.com/user/repos? page=3&per_page=100>; rel="next", <https://api.github.com/user/repos? page=50&per_page=100>; rel="last" Wednesday, October 17, 12
  22. Hypermedia Proxy Pattern {"links": [ {"rel": "self", "href": "http://localhost:9292/status/ 2"}],

    "body":"hello, world", "location":"Los Angeles" } Wednesday, October 17, 12
  23. Hypermedia Proxy Pattern class Status attr_accessor :self_rel, :body def initialize(opts={})

    @body = opts['body'] @location = opts['location'] @self_rel = opts['links'].find{|link| link['rel'] == "self"}['href'] end def location @location ||= begin fetch_data(self_rel)['location'] end end end Wednesday, October 17, 12