Resilient API Design

Resilient API Design

In this talk, we will explore what it takes to design a web service that can evolve over years leveraging hypermedia. We will learn how we can avoid the need for clients to be updated by decoupling the API from our business logic. Keeping the business logic where it belongs, on the server.

D200a17dd269fd4001bacb11662dab4b?s=128

Kyle Fuller

May 02, 2017
Tweet

Transcript

  1. 4.

    What makes a great web API? • Decoupled from implementation

    details • Able to evolve without breaking existing clients
  2. 5.

    Agenda • How to introduce changes to APIs • Hypermedia

    • Client side hypermedia • Server side hypermedia
  3. 10.
  4. 11.
  5. 37.

    WebLinking: Checking for next link if let link = response.findLink(relation:

    "next") { print("We have a next link with the URI: \(link.uri).") }
  6. 41.

    Blog Post (Next Link) { "title": "My First Blog Post",

    "body": "Lorem Ipsum", "_links": [ { "href": "/posts/2", "relation": "next" } ] }
  7. 42.

    Blog Post (Self) { "title": "My First Blog Post", "body":

    "Lorem Ipsum", "_links": [ { "href": "/posts/1", "relation": "self" }, { "href": "/posts/2", "relation": "next" } ] }
  8. 43.

    Blog Post (Comments) { "title": "My First Blog Post", "body":

    "Lorem Ipsum", "_links": [ { "href": "/posts/1", "relation": "self" }, { "href": "/posts/2", "relation": "next" }, { "href": "/posts/1/comments", "relation": "comments" }, ] }
  9. 44.

    Blog Post (Embedded Comments) { "_embed": { "comments": [ {

    "author": "Kyle", "body": "That's a really interesting post!" "_links": [ { "href": "/posts/1/comments/1", "relation": "self" } ] } ] } }
  10. 45.

    Blog Post (Embedded Comments) { "_embed": { "comments": [ {

    "author": "Kyle", "body": "That's a really interesting post!" "_links": [ { "href": "/posts/1/comments/1", "relation": "self" } ] } ] } }
  11. 47.

    Delete Post { "properties": { "title": "My First Blog Post",

    "body": "Lorem Ipsum", }, "actions": [ { "name": "delete", "method": "DELETE", "href": "/posts/1" } ] }
  12. 48.

    Delete Post { "properties": { "title": "My First Blog Post",

    "body": "Lorem Ipsum", }, "actions": [ { "name": "delete", "method": "DELETE", "href": "/posts/1" } ] }
  13. 49.

    Create Comment { "properties": { "title": "My First Blog Post",

    "body": "Lorem Ipsum", }, "actions": [ { "name": "comment", "method": "POST", "href": "/posts/1/comments", "fields": [ { "name": "author", "type": "string" }, { "name": "message", "type": "string" } ] } ] }
  14. 50.

    Create Comment (Logged in) { "properties": { "title": "My First

    Blog Post", "body": "Lorem Ipsum", }, "actions": [ { "name": "comment", "method": "POST", "href": "/posts/1/comments", "fields": [ { "name": "message", "type": "string" } ] } ] }
  15. 53.
  16. 54.
  17. 55.
  18. 59.

    Root Posts Post Comment posts posts create delete report delete

    report self self first prev next last comment self
  19. 65.

    client.enter("https://example.com/") // follow the 'posts' transition .flatMap(followTransition("posts")) // we can

    select the first post .map { $0.representors["posts"].first } // follow the 'delete' transition .flatMap(followTransition("delete"))
  20. 66.

    client.enter("https://example.com/") // follow the 'posts' transition .flatMap(followTransition("posts")) // we can

    select the first post .map { $0.representors["posts"].first } // follow the 'delete' transition .flatMap(followTransition("delete"))
  21. 67.

    client.enter("https://example.com/") // follow the 'posts' transition .flatMap(followTransition("posts")) // we can

    select the first post .map { $0.representors["posts"].first } // follow the 'delete' transition .flatMap(followTransition("delete"))
  22. 68.

    client.enter("https://example.com/") // follow the 'posts' transition .flatMap(followTransition("posts")) // Follow the

    'create' transition .flatMap { client.request($0.transitions["create"], attributes: [ "title": "Hello World", "body": "My First Post", ]) }
  23. 70.

    Post • title (required) • max length: 32 • body

    (required) • min length: 64 • max length: 5000
  24. 71.
  25. 72.
  26. 73.

    JSON Schema { "type": "object", "properties": { "title": { "type":

    "string" "maxLength": 32 } }, "required": ["title"] }
  27. 74.

    JSON Schema { "type": "object", "properties": { "title": { "type":

    "string" "maxLength": 32 } }, "required": ["title"] }
  28. 75.
  29. 76.
  30. 80.

    class PostListResource(Resource): uri_template = '/posts' def get_relations(self): return { 'posts':

    [ HelloPostResource(), SecondPostResource() ] } router.add_root_resource('posts', PostListResource)
  31. 81.

    $ curl http://localhost:8080/ -H 'Accept: application/json' {"url": "/", "posts_url": "/posts"}

    $ curl http://localhost:8080/posts -H 'Accept: application/json' { "url": "/posts", "posts": [ { "body": "Hi", "url": "/posts/1", "title": "Hello World" }, { "body": "Hello", "url": "/posts/2", "title": "Hello Other World" } ] }
  32. 85.

    Hypermedia • REST APIs must be hypertext driven • Roy

    Fielding on Versioning • Solving FizzBuzz with Hypermedia
  33. 86.

    Resources • The Hypermedia Project • Hyperdrive • Starship -

    Generic iOS Hypermedia Browser • HAL Specification • Siren Specification • https://github.com/kylef/WebLinking.swift • https://github.com/kylef/JSONSchema.swift