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

Fast, testable and sane APIs - RubyC.eu Kiev 2014

Fast, testable and sane APIs - RubyC.eu Kiev 2014

By now, we've all written JSON APIs in Rails. But how do you write fast, testable and sane APIs? I'll guide you through the trials of designing and building awesome, scalable APIs. We'll cover Rails-API, ActiveModel::Serializers, and all the heavenly goodness our ecosystem has to offer.

Ben Lovell

May 31, 2014
Tweet

More Decks by Ben Lovell

Other Decks in Programming

Transcript

  1. Fast, testable and SANE APIs
    Ben Lovell

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. I
    ❤️
    Kyiv

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. _benlovell

    View Slide

  14. benlovell
    j

    View Slide

  15. View Slide

  16. View Slide

  17. 113581334398839860922

    View Slide

  18. View Slide

  19. “I HAVE KILLED, AND I WILL KILL AGAIN”

    View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. View Slide

  31. Building
    JSON APIs
    awesome!

    View Slide

  32. Rails-API

    View Slide

  33. JSON API
    BIKESHEDDING

    View Slide

  34. ActiveModel::Serializers
    because #to_json kinda sucks

    View Slide

  35. HTTP
    it’s all about the semantics

    View Slide

  36. /etc
    too meta to categorise

    View Slide

  37. View Slide

  38. Components of
    good API design

    View Slide

  39. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  40. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  41. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  42. JSON API

    View Slide

  43. View Slide

  44. View Slide

  45. application/vnd.api+json
    IANA
    registered

    View Slide

  46. WIP!
    http://git.io/Uh8Y8w

    View Slide

  47. {
    "links": {
    "posts.author": {
    "href": "http://example.com/people/{posts.author}",
    "type": "people"
    },
    "posts.comments": {
    "href": "http://example.com/comments/{posts.comments}",
    "type": "comments"
    }
    },
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
    "author": "9",
    "comments": [ "5", "12", "17", "20" ]
    }
    }]
    }

    View Slide

  48. {
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
    "author": "9",
    "comments": [ "5", "12", "17", "20" ]
    }
    }]
    }

    View Slide

  49. {
    "posts": [{
    "id": 1
    // a post document
    }]
    }
    {
    }, {
    }]
    }
    Singular Resource Resource Collection

    View Slide

  50. {
    }]
    }
    {
    "posts": [{
    "id": 1
    // a post document
    }, {
    "id": 2
    // a post document
    }]
    }
    Singular Resource Resource Collection

    View Slide

  51. {
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
    "comments": [ "5", "12", "17", "20" ]
    }
    }]
    }
    To-Many Relationships

    View Slide

  52. {
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
    "comments": {
    "href": "http://example.com/comments/5,12,17",
    "ids": [ "5", "12", “17" ],
    "type": "comments"
    }
    }
    }]
    }
    To-Many Relationships (link style)

    View Slide

  53. {
    "links": {
    "posts.comments": "http://example.com/posts/{posts.id}/comments"
    },
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase"
    }, {
    "id": "2",
    "title": "The Parley Letter"
    }]
    }
    Shorthand (link style)
    RFC6570
    URI Template

    View Slide

  54. View Slide

  55. View Slide

  56. View Slide

  57. View Slide

  58. /etc
    I did warn you

    View Slide

  59. COMPOUND
    DOCUMENTS
    the fastest HTTP is no HTTP

    View Slide

  60. GET /posts/1?include=comments
    GET /posts/1?include=comments.author
    GET /posts/1?include=author,comments

    View Slide

  61. {
    "links": {
    "posts.author": {
    "href": "http://example.com/people/{posts.author}",
    "type": "people"
    },
    "posts.comments": {
    "href": "http://example.com/comments/{posts.comments}",
    "type": "comments"
    }
    },
    "posts": [{
    "id": "1",
    "title": "Rails is Omakase",
    "links": {
    "author": "9",
    "comments": [ “1" ]
    }}],
    "linked": {
    "authors": [{
    "id": "9",
    "name": "@dhh"
    }],
    "comments": [{
    "id": "1",
    "body": "Mmmmmakase"
    }]
    }
    }

    View Slide

  62. SPARSE
    FIELDSETS

    View Slide

  63. GET /posts/1?fields=id,title,body

    View Slide

  64. HTTP SEMANTICS

    View Slide

  65. View Slide

  66. wat

    View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. RAILS-API

    View Slide

  73. ❤️

    View Slide

  74. Ruby is not as slow
    as you might think…

    View Slide

  75. Ruby is not as slow
    as you might think…
    …IT’S SLOWER

    View Slide

  76. So, how do we make Ruby fast?

    View Slide

  77. By running less Ruby. DERP!
    So, how do we make Ruby fast?

    View Slide

  78. So, how do we make Rails fast?

    View Slide

  79. So, how do we make Rails fast?
    By running less Rails. DERP!

    View Slide

  80. YO DAWG!
    I heard you like rails
    so I took some rails
    out of your rails
    so you could rails
    (a little bit faster than usual)

    View Slide

  81. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use #
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run KitchenSinkFullOfKitchenSinks::Application.routes

    View Slide

  82. View Slide

  83. use Rack::Sendfile
    use ActionDispatch::Static
    use Rack::Lock
    use #
    use Rack::Runtime
    use Rack::MethodOverride
    use ActionDispatch::RequestId
    use Rails::Rack::Logger
    use ActionDispatch::ShowExceptions
    use ActionDispatch::DebugExceptions
    use ActionDispatch::RemoteIp
    use ActionDispatch::Reloader
    use ActionDispatch::Callbacks
    use ActiveRecord::Migration::CheckPending
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    use ActiveRecord::QueryCache
    use ActionDispatch::Cookies
    use ActionDispatch::Session::CookieStore
    use ActionDispatch::Flash
    use ActionDispatch::ParamsParser
    use Rack::Head
    use Rack::ConditionalGet
    use Rack::ETag
    run KitchenSink::Application.routes

    View Slide

  84. View Slide

  85. require “rails/all”

    View Slide

  86. require “rails/all”

    View Slide

  87. irb(main):003:0> pp ActionController::API.ancestors - ActionController::Metal.ancestors
    [
    ActionController::API,
    ActiveRecord::Railties::ControllerRuntime,
    ActionDispatch::Routing::RouteSet::MountedHelpers,
    ActionController::StrongParameters,
    ActionController::Instrumentation,
    ActionController::Rescue,
    ActiveSupport::Rescuable,
    ActionController::DataStreaming,
    ActionController::ForceSSL,
    AbstractController::Callbacks,
    ActiveSupport::Callbacks,
    ActionController::ConditionalGet,
    ActionController::Head,
    ActionController::Renderers::All,
    ActionController::Renderers,
    ActionController::Rendering,
    AbstractController::Rendering,
    AbstractController::ViewPaths,
    ActionController::Redirecting,
    ActionController::RackDelegation,
    ActiveSupport::Benchmarkable,
    AbstractController::Logger,
    ActionController::UrlFor,
    AbstractController::UrlFor,
    ActionDispatch::Routing::UrlFor,
    ActionDispatch::Routing::PolymorphicRoutes,
    ActionController::ModelNaming,
    ActionController::HideActions
    ]

    View Slide

  88. NO CAPTION

    View Slide

  89. Y U NO
    Sinatra!!!

    View Slide

  90. View Slide

  91. Y U NO
    Sinatra!!!
    “Any sufficiently advanced sinatra
    application is indistinguishable
    from rails”

    View Slide

  92. View Slide

  93. don’t reinvent this

    View Slide

  94. ActiveModel
    Serializers

    View Slide

  95. Jbuilder.encode do |json|
    json.content format_content(@message.content)
    json.(@message, :created_at, :updated_at)
    !
    json.author do
    json.name @message.creator.name.familiar
    json.email_address @message.creator.email_address_with_name
    json.url url_for(@message.creator, format: :json)
    end
    !
    if current_user.admin?
    json.visitors calculate_visitors(@message)
    end
    !
    json.comments @message.comments, :content, :created_at
    !
    json.attachments @message.attachments do |attachment|
    json.filename attachment.filename
    json.url url_for(attachment)
    end
    end

    View Slide

  96. View Slide

  97. DHH
    CHANGES
    NAME TO
    BASECAMP

    View Slide

  98. Jbuilder
    json
    json
    !
    json
    json
    json
    json
    end
    !
    if
    json
    end
    !
    json
    !
    json
    json
    json
    end
    end

    View Slide

  99. % rails g serializer …

    View Slide

  100. class PostSerializer < ActiveModel::Serializer
    attributes :id, :title, :body
    has_many :comments
    end

    View Slide

  101. class PostSerializer < ActiveModel::Serializer
    attributes :id, :title, :body, :synopsis
    has_many :comments
    !
    def comments
    object.comments.where(:author => scope)
    end
    !
    def synopsis
    object.body.truncate(30)
    end
    end

    View Slide

  102. class PostsController < ApplicationController
    def show
    @post = Post.find(params[:id])
    respond_with @post
    end
    end

    View Slide

  103. CONVENTION!

    View Slide

  104. VIEWS

    View Slide

  105. class PostSerializer < ActiveModel::Serializer
    attributes :id, :title, :body
    has_many :comments
    has_many :tags
    has_many :images
    belongs_to :author
    end
    WARNING!

    View Slide

  106. class
    attributes
    has_many
    has_many
    has_many
    belongs_to
    end
    CONGRATULATIONS!!!
    YOU SERIALIZED
    YOUR DATABASE

    View Slide

  107. CONDITIONAL
    REQUESTS

    View Slide

  108. $ curl -i https://api.example.com/user
    !
    HTTP/1.1 200 OK
    Cache-Control: private, max-age=60
    ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
    Last-Modified: Thu, 02 Feb 2014 15:31:30 GMT
    Status: 200 OK

    View Slide

  109. $ curl -i https://api.example.com/user
    -H ‘If-None-Match: ”{Etag}”’
    !
    HTTP/1.1 304 Not Modified
    Cache-Control: private, max-age=60
    ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
    Last-Modified: Thu, 02 Feb 2014 15:31:30 GMT
    Status: 304 Not Modified

    View Slide

  110. class PostsController < ApplicationController
    def show
    @post = Post.find(params[:id])
    !
    if stale? @post
    respond_with @post
    end
    end
    end

    View Slide

  111. class PostsController < ApplicationController
    def show
    @post = Post.find(params[:id])
    !
    if stale? @post
    respond_with @post
    end
    end
    end etag: Model#cache_key
    last_modified: Model#updated_at

    View Slide

  112. class PostSerializer < ActiveModel::Serializer
    cached
    !
    delegate :cache_key, :to => :object
    end

    View Slide

  113. ActiveModel
    Serializers

    CACHE

    View Slide

  114. GATEWAY
    CACHING

    View Slide

  115. TESTABLE?

    View Slide

  116. rack-test

    View Slide

  117. View Slide

  118. View Slide

  119. JSON-API
    RAILS-API
    AMS
    = ❤️

    View Slide

  120. JSON-API
    RAILS-API
    AMS
    =

    View Slide

  121. THE REST…?

    View Slide

  122. THANKS!
    @benlovell

    View Slide