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.

146e52d49d361f85c0945487452fc6a0?s=128

Ben Lovell

May 31, 2014
Tweet

Transcript

  1. Fast, testable and SANE APIs Ben Lovell

  2. None
  3. None
  4. None
  5. None
  6. None
  7. I ❤️ Kyiv

  8. None
  9. None
  10. None
  11. None
  12. None
  13. _benlovell

  14. benlovell j

  15. None
  16. None
  17. 113581334398839860922

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

  20. None
  21. None
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. None
  29. None
  30. None
  31. Building JSON APIs awesome!

  32. Rails-API

  33. JSON API BIKESHEDDING

  34. ActiveModel::Serializers because #to_json kinda sucks

  35. HTTP it’s all about the semantics

  36. /etc too meta to categorise

  37. None
  38. Components of good API design

  39. FAST STANDARDISED INTUITIVE

  40. FAST STANDARDISED INTUITIVE

  41. FAST STANDARDISED INTUITIVE

  42. JSON API

  43. None
  44. None
  45. application/vnd.api+json IANA registered

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

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

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

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

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

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

  59. COMPOUND DOCUMENTS the fastest HTTP is no HTTP

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

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

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

  64. HTTP SEMANTICS

  65. None
  66. wat

  67. None
  68. None
  69. None
  70. None
  71. None
  72. RAILS-API

  73. ❤️

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

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

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

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

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

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

    Rails. DERP!
  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)
  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 KitchenSinkFullOfKitchenSinks::Application.routes
  82. None
  83. 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
  84. None
  85. require “rails/all”

  86. require “rails/all”

  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 ]
  88. NO CAPTION

  89. Y U NO Sinatra!!!

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

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

  94. ActiveModel Serializers

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

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

    if json end ! json ! json json json end end
  99. % rails g serializer …

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

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

    @post end end
  103. CONVENTION!

  104. VIEWS

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

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

    YOUR DATABASE
  107. CONDITIONAL REQUESTS

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

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

    :object end
  113. ActiveModel Serializers CACHE

  114. GATEWAY CACHING

  115. TESTABLE?

  116. rack-test

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

  120. JSON-API RAILS-API AMS =

  121. THE REST…?

  122. THANKS! @benlovell