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

Fast, testable and sane APIs - Ancient City Ruby 2014

Fast, testable and sane APIs - Ancient City Ruby 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 associated goodness that our ecosystem has to offer.

I'll speak on the approaches to authentication, how to ensure we remain good REST/HTTP citizens and maybe if I have time I'll share some of my top secret beard grooming tips!

Ben Lovell

April 03, 2014
Tweet

More Decks by Ben Lovell

Other Decks in Programming

Transcript

  1. Fast, testable and SANE APIs
    Ben Lovell

    View Slide

  2. _benlovell

    View Slide

  3. benlovell
    j

    View Slide

  4. 113581334398839860922

    View Slide

  5. View Slide

  6. View Slide

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

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. no caption

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

    View Slide

  28. Rails-API
    warning: there may be ranting

    View Slide

  29. JSON API
    your tool against bikeshedding

    View Slide

  30. ActiveModel::Serializers
    because #to_json kinda sucks

    View Slide

  31. HTTP/REST
    you’re doing it wrong (maybe)

    View Slide

  32. /etc
    too meta to categorise

    View Slide

  33. View Slide

  34. Components of
    good API design

    View Slide

  35. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  36. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  37. FAST
    STANDARDISED
    INTUITIVE

    View Slide

  38. JSON API

    View Slide

  39. View Slide

  40. View Slide

  41. application/vnd.api+json
    IANA
    registered

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

  50. View Slide

  51. View Slide

  52. View Slide

  53. View Slide

  54. /etc
    I did warn you

    View Slide

  55. COMPOUND
    DOCUMENTS
    the fastest HTTP is no HTTP

    View Slide

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

    View Slide

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

  58. SPARSE
    FIELDSETS

    View Slide

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

    View Slide

  60. HTTP SEMANTICS

    View Slide

  61. View Slide

  62. wat

    View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. RAILS-API

    View Slide

  69. ❤️

    View Slide

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

    View Slide

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

    View Slide

  72. So, how do we make Ruby fast?

    View Slide

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

    View Slide

  74. So, how do we make Rails fast?

    View Slide

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

    View Slide

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

  77. View Slide

  78. View Slide

  79. http://goo.gl/qfJG2d

    View Slide

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

  81. E.B.T.K.S

    View Slide

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

  83. A.B.T.K.S

    View Slide

  84. require “rails/all”

    View Slide

  85. require “rails/all”

    View Slide

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

  87. NO CAPTION

    View Slide

  88. Y U NO
    Sinatra!!!

    View Slide

  89. View Slide

  90. View Slide

  91. View Slide

  92. View Slide


  93. HEY
    LADIES

    View Slide


  94. ❤️

    View Slide

  95. View Slide

  96. View Slide

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

    View Slide

  98. View Slide

  99. don’t reinvent this

    View Slide

  100. ActiveModel
    Serializers

    View Slide

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

  102. View Slide

  103. DHH
    CHANGES
    NAME TO
    BASECAMP

    View Slide

  104. Jbuilder
    json
    json
    !
    json
    json
    json
    json
    end
    !
    if
    json
    end
    !
    json
    !
    json
    json
    json
    end
    end

    View Slide

  105. % rails g serializer …

    View Slide

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

    View Slide

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

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

    View Slide

  109. CONVENTION!

    View Slide

  110. VIEWS

    View Slide

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

    View Slide

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

    View Slide

  113. GET, BUT GET
    CONDITIONALLY

    View Slide

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

    View Slide

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

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

    View Slide

  117. ActiveModel
    Serializers

    CACHE

    View Slide

  118. GATEWAY
    CACHING
    excuse me while I sidestep the tricky

    View Slide

  119. View Slide

  120. View Slide

  121. View Slide

  122. View Slide

  123. View Slide

  124. TESTABLE?

    View Slide

  125. View Slide

  126. rack-test

    View Slide

  127. THE REST…?

    View Slide

  128. THANKS!
    @benlovell

    View Slide