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

Fast, testable and sane JSON-APIs with Rails-API et al

Fast, testable and sane JSON-APIs with Rails-API et al

This talk was given at Brighton Ruby Conference 2014.

Ben Lovell

July 21, 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. View Slide

  8. View Slide

  9. View Slide

  10. _benlovell
    t

    View Slide

  11. benlovell

    View Slide

  12. View Slide

  13. View Slide

  14. 113581334398839860922

    View Slide

  15. View Slide

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

    View Slide

  17. View Slide

  18. View Slide

  19. 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. Building
    JSON APIs
    awesome!

    View Slide

  29. Rails-API

    View Slide

  30. JSON API
    BIKESHEDDING

    View Slide

  31. ActiveModel::Serializers
    because #to_json kinda sucks

    View Slide

  32. HTTP
    it’s all about the semantics

    View Slide

  33. /etc
    too meta to categorise

    View Slide

  34. View Slide

  35. Components of
    good API design

    View Slide

  36. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  37. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  38. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  39. View Slide

  40. JSON API

    View Slide

  41. View Slide

  42. View Slide

  43. application/vnd.api+json
    IANA
    registered

    View Slide

  44. W
    IP!
    http://git.io/Uh8Y8w

    View Slide

  45. {
    "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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. {
    "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

  51. {
    "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

  52. View Slide

  53. View Slide

  54. View Slide

  55. /etc
    I did warn you

    View Slide

  56. COMPOUND
    DOCUMENTS
    the fastest HTTP is no HTTP

    View Slide

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

    View Slide

  58. {
    "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

  59. SPARSE
    FIELDSETS

    View Slide

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

    View Slide

  61. HTTP SEMANTICS

    View Slide

  62. View Slide

  63. wat

    View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. RAILS-API

    View Slide

  68. ❤️

    View Slide

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

    View Slide

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

    View Slide

  71. So, how do we make Ruby fast?

    View Slide

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

    View Slide

  73. So, how do we make Rails fast?

    View Slide

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

    View Slide

  75. 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

  76. View Slide

  77. View Slide

  78. http://goo.gl/qfJG2d

    View Slide

  79. 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

  80. 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 KitchenSink::Application.routes

    View Slide

  82. View Slide

  83. require “rails/all”

    View Slide

  84. require “rails/all”

    View Slide

  85. 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

  86. View Slide

  87. Y U NO
    Sinatra!!!

    View Slide

  88. View Slide

  89. View Slide

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

    View Slide

  91. View Slide

  92. don’t reinvent this

    View Slide

  93. ActiveModel
    Serializers

    View Slide

  94. 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

  95. View Slide

  96. DHH
    CHANGES
    NAME TO
    BASECAMP

    View Slide

  97. Jbuilder
    json
    json
    json
    json
    json
    json
    end
    if
    json
    end
    json
    json
    json
    json
    end
    end

    View Slide

  98. % rails g serializer …

    View Slide

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

    View Slide

  100. 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

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

    View Slide

  102. CONVENTION!

    View Slide

  103. VIEWS

    View Slide

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

    View Slide

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

    View Slide

  106. CONDITIONAL
    REQUESTS

    View Slide

  107. $ 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

  108. $ 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

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

    View Slide

  110. 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

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

    View Slide

  112. ActiveModel
    Serializers

    CACHE

    View Slide

  113. GATEWAY
    CACHING

    View Slide

  114. TESTABLE?

    View Slide

  115. rack-test

    View Slide

  116. View Slide

  117. View Slide

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

    View Slide

  119. JSON-API
    RAILS-API
    AMS
    =

    View Slide

  120. THE REST…?

    View Slide

  121. THANKS!
    @benlovell

    View Slide