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!

146e52d49d361f85c0945487452fc6a0?s=128

Ben Lovell

April 03, 2014
Tweet

Transcript

  1. Fast, testable and SANE APIs Ben Lovell

  2. _benlovell

  3. benlovell j

  4. 113581334398839860922

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

  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. None
  18. no caption

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

  28. Rails-API warning: there may be ranting

  29. JSON API your tool against bikeshedding

  30. ActiveModel::Serializers because #to_json kinda sucks

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

  32. /etc too meta to categorise

  33. None
  34. Components of good API design

  35. FAST STANDARDISED INTUITIVE

  36. FAST STANDARDISED INTUITIVE

  37. FAST STANDARDISED INTUITIVE

  38. JSON API

  39. None
  40. None
  41. application/vnd.api+json IANA registered

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

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

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

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

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

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

  55. COMPOUND DOCUMENTS the fastest HTTP is no HTTP

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

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

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

  60. HTTP SEMANTICS

  61. None
  62. wat

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

  69. ❤️

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

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

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

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

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

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

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

  80. 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
  81. E.B.T.K.S

  82. 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
  83. A.B.T.K.S

  84. require “rails/all”

  85. require “rails/all”

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

  88. Y U NO Sinatra!!!

  89. None
  90. None
  91. None
  92. None
  93. HEY LADIES

  94. ❤️

  95. None
  96. None
  97. Y U NO Sinatra!!! “Any sufficiently advanced sinatra application is

    indistinguishable from rails”
  98. None
  99. don’t reinvent this

  100. ActiveModel Serializers

  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
  102. None
  103. DHH CHANGES NAME TO BASECAMP

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

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

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

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

    @post end end
  109. CONVENTION!

  110. VIEWS

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

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

    YOUR DATABASE
  113. GET, BUT GET CONDITIONALLY

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

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

    :object end
  117. ActiveModel Serializers CACHE

  118. GATEWAY CACHING excuse me while I sidestep the tricky

  119. None
  120. None
  121. None
  122. None
  123. None
  124. TESTABLE?

  125. None
  126. rack-test

  127. THE REST…?

  128. THANKS! @benlovell