Building Web Apps with Rack
Prerequisites: Working with JSON and YAML, Gems and Bundler
Every Ruby web framework — Rails, Sinatra, Hanami, Roda — sits on Rack, a tiny standard interface between web servers and Ruby applications. Understanding it demystifies how Ruby web apps work.
The Rack contract
A Rack application is any object with a call method that:
- takes one argument,
env— a hash describing the request, and - returns an array of three things: status, headers, and a
body (an object that responds to
each, usually an array of strings).
A complete app
bundle init
bundle add rack rackup puma
# config.ru
require "json"
class App
def call(env)
request = Rack::Request.new(env)
case [request.request_method, request.path_info]
in ["GET", "/"]
[200, { "content-type" => "text/plain" }, ["Hello from Rack"]]
in ["GET", "/health"]
json(200, { status: "ok" })
in ["POST", "/echo"]
json(201, { received: JSON.parse(request.body.read) })
else
json(404, { error: "not found" })
end
rescue JSON::ParserError
json(400, { error: "invalid JSON" })
end
private
def json(status, data)
[status, { "content-type" => "application/json" }, [data.to_json]]
end
end
run App.new
$ bundle exec rackup
Puma starting in single mode...
* Listening on http://127.0.0.1:9292
$ curl localhost:9292/health
{"status":"ok"}
config.ru ("rack up") is the standard entry point, and run tells
Rack which app to serve. Rack 3 requires lowercase header names.
Middleware
Middleware wraps an app to add behavior before and after each request — logging, authentication, compression. A middleware is itself a Rack app that holds the next app:
class RequestTimer
def initialize(app)
@app = app
end
def call(env)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env)
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(1)
headers["server-timing"] = "app;dur=#{ms}"
[status, headers, body]
end
end
use RequestTimer # in config.ru, before `run`
Rails' request pipeline is a stack of exactly these objects —
bin/rails middleware lists them.
Testing Rack apps
The rack-test gem sends fake requests straight to the app, with no server running:
require "rack/test"
class AppTest < Minitest::Test
include Rack::Test::Methods
def app = App.new
def test_health
get "/health"
assert_equal 200, last_response.status
assert_equal({ "status" => "ok" }, JSON.parse(last_response.body))
end
end
Where to go from here
For a real project, pick a framework on top of Rack: Sinatra or Roda for small services, Rails for full applications with databases, views, jobs, and mailers.