Rack 'em, Stack 'em, Web Apps

Rack 'em, Stack 'em, Web Apps

While Rails is the most common Ruby web framework, it’s not the only option. Rack is a simple, elegant HTTP library, ideal for microservices and high performance applications.

In this talk, you’ll see Rack from top to bottom. Starting from the simplest app, we’ll grow our code into a RESTful HTTP API. We’ll test our code, write reusable middleware, and dig through what Rack provides out of the box. Throughout, we’ll balance when Rack is a good fit, and when larger tools are needed.

If you’ve heard of Rack but wondered where it fits in the Ruby web stack, here’s your chance!

92e7389893670a1920a4fd98aec0d246?s=128

Jason R Clark

April 25, 2017
Tweet

Transcript

  1. Rack 'em Stack 'em Web Apps @jasonrclark art by asher

    clark
  2. http://fight.robotlikes.com 2

  3. What Is Rack? 3

  4. 4

  5. 5 x browser server rack ----- ------ --- ---- rails

  6. Why Rack? 6

  7. 7 Speed Simplicity Reuse

  8. Show Me The Code!? 8

  9. 9 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  10. 10 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  11. 11 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  12. 12 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  13. 13 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  14. 14 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  15. 15 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  16. 16 $ terminal $ rackup [2017-04-17] INFO WEBrick 1.3.1 [2017-04-17]

    INFO ruby 2.4.1 [2017-04-17] INFO WEBrick::HTTPServer#start: pid=32555 port=9292
  17. 17

  18. 18 $ terminal $ curl http://localhost:9292 Good enough

  19. Rack::Test 19

  20. 20 # Gemfile gem "rack" gem "rack-test"

  21. 21 # test/test_helper.rb require "minitest/autorun" require "rack/test"

  22. 22 # app_test.rb class ApplicationTest < Minitest::Test include Rack::Test::Methods def

    app Application.new end # ... end
  23. 23 # app_test.rb class ApplicationTest < Minitest::Test include Rack::Test::Methods def

    app Application.new end # ... end
  24. 24 # app_test.rb class ApplicationTest < Minitest::Test include Rack::Test::Methods def

    app Application.new end # ... end
  25. 25 # app_test.rb class ApplicationTest < Minitest::Test def test_its_good_enough get

    "/" assert last_response.ok? assert_equal "Good enough", last_response.body end end
  26. 26 # app_test.rb class ApplicationTest < Minitest::Test def test_its_good_enough get

    "/" assert last_response.ok? assert_equal "Good enough", last_response.body end end
  27. 27 # app_test.rb class ApplicationTest < Minitest::Test def test_its_good_enough get

    "/" assert last_response.ok? assert_equal "Good enough", last_response.body end end
  28. Think About The Environment! 28

  29. 29 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  30. 30 # config.ru class Application def call(env) [200, {}, ["Good

    enough"]] end end run Application.new
  31. 31 # env { "REMOTE_ADDR"=>"::1", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost/bot", "PATH_INFO"=>"/bot", "QUERY_STRING"=>"", "HTTP_USER_AGENT"=>"Mozilla/5.0

    (Mac)", "rack.input"=> #<Rack::Lint::InputWrapper>, # ... }
  32. 32 # env { "REMOTE_ADDR"=>"::1", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost/bot", "PATH_INFO"=>"/bot", "QUERY_STRING"=>"", "HTTP_USER_AGENT"=>"Mozilla/5.0

    (Mac)", "rack.input"=> #<Rack::Lint::InputWrapper>, # ... }
  33. 33 # env { "REMOTE_ADDR"=>"::1", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost/bot", "PATH_INFO"=>"/bot", "QUERY_STRING"=>"", "HTTP_USER_AGENT"=>"Mozilla/5.0

    (Mac)", "rack.input"=> #<Rack::Lint::InputWrapper>, # ... }
  34. 34 # env { "REMOTE_ADDR"=>"::1", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost/bot", "PATH_INFO"=>"/bot", "QUERY_STRING"=>"", "HTTP_USER_AGENT"=>"Mozilla/5.0

    (Mac)", "rack.input"=> #<Rack::Lint::InputWrapper>, # ... }
  35. 35 # config.ru class Application def call(env) if env["PATH_INFO"] ==

    "/bot" [403, {}, ["Beep beep intruder"]] else [200, {}, ["Good enough"]] end end end
  36. 36 # config.ru class Application def call(env) if env["PATH_INFO"] ==

    "/bot" [403, {}, ["Beep beep intruder"]] else [200, {}, ["Good enough"]] end end end
  37. Routing 37

  38. 38 Frameworks help!

  39. 39 # config.ru class Status def call(env) # ... end

    end map "/status" do run Status.new end map "/" do run Application.new end
  40. 40 # config.ru class Status def call(env) # ... end

    end map "/status" do run Status.new end map "/" do run Application.new end
  41. 41 # config.ru class Status def call(env) # ... end

    end run Rack::URLMap.new( "/status" => Status.new, "/" => Application.new )
  42. Rack::Request 42

  43. 43 # app.rb class Application def call(env) request = Rack::Request.new(env)

    if request.path_info == "/bot" [403, {}, ["Beep intruder"]] else [200, {}, ["Good enough"]] end end end
  44. 44 # app.rb class Application def call(env) request = Rack::Request.new(env)

    if request.path_info.match %r{/bot/(\d+)} [403, {}, ["Beep intruder on #{$1}"]] else [200, {}, ["Good enough"]] end end end
  45. 45 Isn't there an easier way!?

  46. 46 Frameworks help!

  47. 47 # app.rb class Application def call(env) request = Rack::Request.new(env)

    if request.path_info.match %r{/bot/(\d+)} bot = Database.find($1) [200, {}, [bot.to_s]] else # ... end end end
  48. 48 $ terminal $ curl http://localhost:9292/bot/1 <--[1]-->

  49. GET 49

  50. POST 50

  51. 51 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  52. 52 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  53. 53 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  54. 54 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  55. 55 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  56. 56 # app.rb if request.path_info.match %r{/bot/(\d+)} if request.get? bot =

    Database.find($1) [200, {}, [bot.to_s]] elsif request.post? bot = request.body.read Database.save($1, bot) [200, {}, ["Wrote bot #{$1}"]] end end
  57. 57 $ terminal $ curl -X POST http://localhost:9292/bot/2 --data "\[*]/"

    Wrote bot 2
  58. 58 $ terminal $ curl -X POST http://localhost:9292/bot/2 --data "\[*]/"

    Wrote bot 2 $ curl http://localhost:9292/bot/2 \[*]/
  59. Rack::Response 59

  60. 60 # app.rb def call(env) response = Rack::Response.new if #...

    bot = Database.find($1) response.write(bot.to_s) end response.finish end
  61. 61 # app.rb def call(env) response = Rack::Response.new if #...

    bot = Database.find($1) response.write(bot.to_s) end response.finish end
  62. 62 # app.rb def call(env) response = Rack::Response.new if #...

    bot = Database.find($1) response.write(bot.to_s) end response.finish end
  63. 63 # app.rb def call(env) response = Rack::Response.new if #...

    bot = Database.find($1) response.write(bot.to_s) end response.finish end
  64. 64 $ pry > response.finish [200, {"Content-Length"=>"11"}, #<Rack::BodyProxy:0x007f80f0b92ff8...>]

  65. 65 $ pry > response.finish [200, {"Content-Length"=>"11"}, #<Rack::BodyProxy:0x007f80f0b92ff8...>]

  66. Middleware 66

  67. 67 # config.ru require 'app' use Middleware run Application.new

  68. 68 # config.ru require 'app' use Middleware run Application.new

  69. 69 middleware middleware ----- ------ --- ---- app ----- ------

    --- ---- ----- ------ --- ---- call() call() call()
  70. 70 # middleware.rb class Middleware def initialize(app, _opts={}) @app =

    app end def call(env) @app.call(env) end end
  71. 71 # middleware.rb class Middleware def initialize(app, _opts={}) @app =

    app end def call(env) @app.call(env) end end
  72. 72 # middleware.rb class Middleware def initialize(app, _opts={}) @app =

    app end def call(env) @app.call(env) end end
  73. 73 # middleware.rb def call(env) req = Rack::Request.new(env) key =

    req.get_header("HTTP_X_API_KEY") if key == "beep" @app.call(env) else [403, {}, ["FORBIDDEN! BEEP"]] end end
  74. 74 # middleware.rb def call(env) req = Rack::Request.new(env) key =

    req.get_header("HTTP_X_API_KEY") if key == "beep" @app.call(env) else [403, {}, ["FORBIDDEN! BEEP"]] end end
  75. 75 # middleware.rb def call(env) req = Rack::Request.new(env) key =

    req.get_header("HTTP_X_API_KEY") if key == "beep" @app.call(env) else [403, {}, ["FORBIDDEN! BEEP"]] end end
  76. 76 # middleware.rb def call(env) req = Rack::Request.new(env) key =

    req.get_header("HTTP_X_API_KEY") if key == "beep" @app.call(env) else [403, {}, ["FORBIDDEN! BEEP"]] end end
  77. 77 middleware ----- ------ --- ---- app ----- ------ ---

    ---- call() nope!
  78. 78 $ terminal $ curl http://localhost:9292/bot/1 FORBIDDEN! BEEP

  79. 79 $ terminal $ curl http://localhost:9292/bot/1 FORBIDDEN! BEEP $ curl

    -H "X-Api-Key: beep" http://localhost:9292/bot/1 <--[1]-->
  80. 80 Rack::Static Rack::Session::* Rack::ShowExceptions Rack::Reloader

  81. Frameworks 81

  82. 82 middleware middleware ----- ------ --- ---- rails ----- ------

    --- ---- call() call() call()
  83. 83 # config/routes.rb Rails.application.routes.draw do mount RackApplication.new, at: "/api" end

  84. 84 # config/application.rb class Application < Rails::Application config.middleware.use RackMiddleware config.middleware.insert_after

    Rack::Static, AnotherMiddleware end
  85. 85 # config/application.rb class Application < Rails::Application config.middleware.use RackMiddleware config.middleware.insert_after

    Rack::Static, AnotherMiddleware end
  86. 86 # config/application.rb class Application < Rails::Application config.middleware.use RackMiddleware config.middleware.insert_after

    Rack::Static, AnotherMiddleware end
  87. 87 middleware middleware ----- ------ --- ---- rails ----- ------

    --- ---- call() call() call()
  88. 88 middleware middleware ---- ---- --- ---- rails ---- ----

    --- ---- middleware -- --- -- ---
  89. 89 middleware middleware ----- ------ --- ---- sinatra ----- ------

    --- ---- call() call() call()
  90. 90 # sinatra_app.rb class Application < Sinatra::Base get "/" do

    [200, {}, ["Wut!?"]] end end
  91. 91 # sinatra_app.rb class Application < Sinatra::Base get "/" do

    [200, {}, ["Wut!?"]] end end
  92. https://bit.ly/rack-apps What, Why, and How Request and Response Middleware Frameworks

    92 ??