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. Resilient API Design

  2. Resilient API Design

  3. What makes a great web API?

  4. What makes a great web API? • Decoupled from implementation

    details • Able to evolve without breaking existing clients
  5. Agenda • How to introduce changes to APIs • Hypermedia

    • Client side hypermedia • Server side hypermedia
  6. Example: Pagination

  7. GET /posts

  8. GET /posts{?page}

  9. GET /posts?page=2

  10. None
  11. None
  12. ! How can we solve?

  13. GET /posts{?before}

  14. GET /posts?before=44

  15. API Timeline

  16. How do we introduce changes to our API?

  17. Versioning APIs

  18. /v1/posts{?page} /v2/posts{?before}

  19. /posts{?page} /posts{?before}

  20. API is coupled to implementation details

  21. API is coupled to implementation details

  22. What happens when you version an API?

  23. What happens when you version an API?

  24. API Client Timeline

  25. How can we design the API without exposing implementation details?

  26. REST Representational State Transfer

  27. Anticipating change is one of the central themes of REST

  28. Evolvability

  29. Tight Coupling

  30. "You can’t have evolvability if clients have their controls baked

    into their design at deployment"
  31. "Controls have to be learned on the fly. That’s what

    hypermedia enables"
  32. Hypermedia

  33. Web Linking RFC 5988

  34. GET /posts Link: </posts?before=30>; rel="next", </posts?before=120>; rel="last"

  35. GET /posts?before=120 Link: </posts?before=90>; rel="prev", </posts>; rel="first"

  36. Hypermedia API Timeline

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

    "next") { print("We have a next link with the URI: \(link.uri).") }
  38. WebLinking: Introspecting Available Links for link in response.links { print("Relation:

    \(link.relationType)") print("URI: \(link.uri)") }
  39. application/hal+json

  40. Blog Post { "title": "My First Blog Post", "body": "Lorem

    Ipsum" }
  41. Blog Post (Next Link) { "title": "My First Blog Post",

    "body": "Lorem Ipsum", "_links": [ { "href": "/posts/2", "relation": "next" } ] }
  42. Blog Post (Self) { "title": "My First Blog Post", "body":

    "Lorem Ipsum", "_links": [ { "href": "/posts/1", "relation": "self" }, { "href": "/posts/2", "relation": "next" } ] }
  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" }, ] }
  44. Blog Post (Embedded Comments) { "_embed": { "comments": [ {

    "author": "Kyle", "body": "That's a really interesting post!" "_links": [ { "href": "/posts/1/comments/1", "relation": "self" } ] } ] } }
  45. Blog Post (Embedded Comments) { "_embed": { "comments": [ {

    "author": "Kyle", "body": "That's a really interesting post!" "_links": [ { "href": "/posts/1/comments/1", "relation": "self" } ] } ] } }
  46. application/ vnd.siren+json

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

    "body": "Lorem Ipsum", }, "actions": [ { "name": "delete", "method": "DELETE", "href": "/posts/1" } ] }
  48. Delete Post { "properties": { "title": "My First Blog Post",

    "body": "Lorem Ipsum", }, "actions": [ { "name": "delete", "method": "DELETE", "href": "/posts/1" } ] }
  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" } ] } ] }
  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" } ] } ] }
  51. Hypermedia • Remove implementation details from interface • Keep business

    logic on back-end, not front-end
  52. It's not about exposing your database via an API

  53. Semantics

  54. Posts • create (maybe) • list (maybe) • first •

    prev • self • next • last
  55. Posts • create (maybe) • list (maybe) • first •

    prev • self • next • last
  56. Post • delete (maybe) • report (maybe) • comments (maybe)

    • create (maybe)
  57. Comment • author • delete • report

  58. Comment • author • delete • report

  59. Root Posts Post Comment posts posts create delete report delete

    report self self first prev next last comment self
  60. Hypermedia Client

  61. Hyperdrive

  62. client.enter("https://example.com/")

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

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

    select the first post .map { $0.representors["posts"].first }
  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"))
  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"))
  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"))
  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", ]) }
  69. Validation

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

    (required) • min length: 64 • max length: 5000
  71. None
  72. None
  73. JSON Schema { "type": "object", "properties": { "title": { "type":

    "string" "maxLength": 32 } }, "required": ["title"] }
  74. JSON Schema { "type": "object", "properties": { "title": { "type":

    "string" "maxLength": 32 } }, "required": ["title"] }
  75. Starship

  76. None
  77. Server Side

  78. class HelloPostResource(Resource): uri_template = '/posts/1' def get_attributes(self): return { 'title':

    'Hello World', 'body': 'Hi' }
  79. class SecondPostResource(Resource): uri_template = '/posts/2' def get_attributes(self): return { 'title':

    'Hello Other World', 'body': 'Hello' }
  80. class PostListResource(Resource): uri_template = '/posts' def get_relations(self): return { 'posts':

    [ HelloPostResource(), SecondPostResource() ] } router.add_root_resource('posts', PostListResource)
  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" } ] }
  82. class PostListResource(Resource): uri_template = '/posts' def get_relations(self): ... def can_embed(self,

    relation): return False
  83. $ curl http://localhost:8080/posts -H 'Accept: application/json' { "url": "/posts", "posts":

    [ { "url": "/posts/1" }, { "url": "/posts/2" } ] }
  84. Hypermedia

  85. Hypermedia • REST APIs must be hypertext driven • Roy

    Fielding on Versioning • Solving FizzBuzz with Hypermedia
  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
  87. fuller.li/talks