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

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!

Jason R Clark

April 25, 2017
Tweet

More Decks by Jason R Clark

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. What Is
    Rack?
    3

    View Slide

  4. 4

    View Slide

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

    View Slide

  6. Why Rack?
    6

    View Slide

  7. 7
    Speed
    Simplicity
    Reuse

    View Slide

  8. Show Me
    The Code!?
    8

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  17. 17

    View Slide

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

    View Slide

  19. Rack::Test
    19

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  28. Think About
    The
    Environment!
    28

    View Slide

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

    View Slide

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

    View Slide

  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"=> #,
    # ...
    }

    View Slide

  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"=> #,
    # ...
    }

    View Slide

  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"=> #,
    # ...
    }

    View Slide

  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"=> #,
    # ...
    }

    View Slide

  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

    View Slide

  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

    View Slide

  37. Routing
    37

    View Slide

  38. 38
    Frameworks help!

    View Slide

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

    View Slide

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

    View Slide

  41. 41
    # config.ru
    class Status
    def call(env)
    # ...
    end
    end
    run Rack::URLMap.new(
    "/status" => Status.new,
    "/" => Application.new
    )

    View Slide

  42. Rack::Request
    42

    View Slide

  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

    View Slide

  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

    View Slide

  45. 45
    Isn't there an easier
    way!?

    View Slide

  46. 46
    Frameworks help!

    View Slide

  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

    View Slide

  48. 48
    $ terminal
    $ curl http://localhost:9292/bot/1
    <--[1]-->

    View Slide

  49. GET
    49

    View Slide

  50. POST
    50

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  59. Rack::Response
    59

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  64. 64
    $ pry
    > response.finish
    [200,
    {"Content-Length"=>"11"},
    #]

    View Slide

  65. 65
    $ pry
    > response.finish
    [200,
    {"Content-Length"=>"11"},
    #]

    View Slide

  66. Middleware
    66

    View Slide

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

    View Slide

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

    View Slide

  69. 69
    middleware middleware
    -----
    ------
    ---
    ----
    app
    -----
    ------
    ---
    ----
    -----
    ------
    ---
    ----
    call() call() call()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  77. 77
    middleware
    -----
    ------
    ---
    ----
    app
    -----
    ------
    ---
    ----
    call()
    nope!

    View Slide

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

    View Slide

  79. 79
    $ terminal
    $ curl http://localhost:9292/bot/1
    FORBIDDEN! BEEP
    $ curl -H "X-Api-Key: beep"
    http://localhost:9292/bot/1
    <--[1]-->

    View Slide

  80. 80
    Rack::Static
    Rack::Session::*
    Rack::ShowExceptions
    Rack::Reloader

    View Slide

  81. Frameworks
    81

    View Slide

  82. 82
    middleware middleware
    -----
    ------
    ---
    ----
    rails
    -----
    ------
    ---
    ----
    call() call() call()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  87. 87
    middleware middleware
    -----
    ------
    ---
    ----
    rails
    -----
    ------
    ---
    ----
    call() call() call()

    View Slide

  88. 88
    middleware middleware
    ----
    ----
    ---
    ----
    rails
    ----
    ----
    ---
    ----
    middleware
    --
    ---
    --
    ---

    View Slide

  89. 89
    middleware middleware
    -----
    ------
    ---
    ----
    sinatra
    -----
    ------
    ---
    ----
    call() call() call()

    View Slide

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

    View Slide

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

    View Slide

  92. https://bit.ly/rack-apps
    What, Why, and How
    Request and Response
    Middleware
    Frameworks
    92
    ??

    View Slide