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.

146e52d49d361f85c0945487452fc6a0?s=128

Ben Lovell

July 21, 2014
Tweet

Transcript

  1. Fast, testable and SANE APIs Ben Lovell

  2. None
  3. ✈️

  4. ✈️

  5. ✈️

  6. None
  7. None
  8. None
  9. None
  10. _benlovell t

  11. benlovell

  12. None
  13. None
  14. 113581334398839860922

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

  17. None
  18. None
  19. None
  20. None
  21. None
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. Building JSON APIs awesome!

  29. Rails-API

  30. JSON API BIKESHEDDING

  31. ActiveModel::Serializers because #to_json kinda sucks

  32. HTTP it’s all about the semantics

  33. /etc too meta to categorise

  34. None
  35. Components of good API design

  36. FAST STANDARDISED INTUITIVE

  37. FAST STANDARDISED INTUITIVE

  38. FAST STANDARDISED INTUITIVE

  39. None
  40. JSON API

  41. None
  42. None
  43. application/vnd.api+json IANA registered

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

  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" ] } }] }
  46. { "posts": [{ "id": "1", "title": "Rails is Omakase", "links":

    { "author": "9", "comments": [ "5", "12", "17", "20" ] } }] }
  47. { "posts": [{ "id": 1 // a post document }]

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

    post document }, { "id": 2 // a post document }] } Singular Resource Resource Collection
  49. { "posts": [{ "id": "1", "title": "Rails is Omakase", "links":

    { "comments": [ "5", "12", "17", "20" ] } }] } To-Many Relationships
  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)
  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
  52. None
  53. None
  54. None
  55. /etc I did warn you

  56. COMPOUND DOCUMENTS the fastest HTTP is no HTTP

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

  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" }] } }
  59. SPARSE FIELDSETS

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

  61. HTTP SEMANTICS

  62. None
  63. wat

  64. None
  65. None
  66. None
  67. RAILS-API

  68. ❤️

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

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

    SLOWER
  71. So, how do we make Ruby fast?

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

    Ruby fast?
  73. So, how do we make Rails fast?

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

    Rails. DERP!
  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)
  76. None
  77. None
  78. http://goo.gl/qfJG2d

  79. use Rack::Sendfile use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware> 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
  80. None
  81. use Rack::Sendfile use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware> 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
  82. None
  83. require “rails/all”

  84. require “rails/all”

  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 ]
  86. None
  87. Y U NO Sinatra!!!

  88. None
  89. None
  90. Y U NO Sinatra!!! “Any sufficiently advanced sinatra application is

    indistinguishable from rails”
  91. None
  92. don’t reinvent this

  93. ActiveModel Serializers

  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
  95. None
  96. DHH CHANGES NAME TO BASECAMP

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

    end json json json json end end
  98. % rails g serializer …

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

    end
  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
  101. class PostsController < ApplicationController def show @post = Post.find(params[:id]) respond_with

    @post end end
  102. CONVENTION!

  103. VIEWS

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

    has_many :tags has_many :images belongs_to :author end WARNING!
  105. class attributes has_many has_many has_many belongs_to end CONGRATULATIONS!!! YOU SERIALIZED

    YOUR DATABASE
  106. CONDITIONAL REQUESTS

  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
  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
  109. class PostsController < ApplicationController def show @post = Post.find(params[:id]) if

    stale? @post respond_with @post end end end
  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
  111. class PostSerializer < ActiveModel::Serializer cached delegate :cache_key, :to => :object

    end
  112. ActiveModel Serializers CACHE

  113. GATEWAY CACHING

  114. TESTABLE?

  115. rack-test

  116. None
  117. None
  118. JSON-API RAILS-API AMS = ❤️

  119. JSON-API RAILS-API AMS =

  120. THE REST…?

  121. THANKS! @benlovell