Getting Started With Rack
This guide demonstrates how to create a basic Rack application and run it using a Rack-compliant web server. We’ll use Puma in this tutorial (alternative web servers can be found in the Readme).
Creating a Rack Application
A Rack app is an object which implements a call method. It is passed an env hash, known as the Rack environment.
rack_app = lambda do |env| [200, { "content-type" => "text/plain" }, ["Hello World"]] end run rack_app
A class can also be used to define a Rack app:
class App def self.call(env) new(env).route end def initialize(env) @env = env end def route [200, { "content-type" => "text/plain" }, ["Hello World"]] end end run App
When an HTTP request is made, the Rack-compliant web server parses it to create the env hash, and calls the application with env. The call method must return an array with exactly three elements, representing the HTTP response:
-
The HTTP response code (
200in the above example). -
A hash containing any HTTP response headers we wish to send.
-
An enumerable object that yields strings, representing the response body.
Rack applications are generally run using the web server’s command line program, with the entry point for the application being stored in a config.ru file:
$ cat > config.ru << APP
rack_app = lambda do |env|
[200, { "content-type" => "text/plain" }, ["Hello World"]]
end
run rack_app
APP
$ gem install puma
$ puma
Your app should be available at localhost:9292.
$ curl localhost:9292 Hello World
Handling Routes and HTTP Verbs
Routing to different paths can be handled by querying the env hash:
app = lambda do |env| body = case env["PATH_INFO"] when "/admin" ["Hello Admin"] else ["Hello World"] end [200, { "content-type" => "text/plain" }, body] end run app
The HTTP verb is also available within the env:
app = lambda do |env| [200, { "content-type" => "text/plain" }, ["HTTP #{env["REQUEST_METHOD"]}"]] end run app
Rack provides Rack::Request, which implements a convenient interface to a Rack environment.
The above examples can be rewritten as:
app = lambda do |env| request = Rack::Request.new(env) body = case request.path_info when "/admin" ["Hello Admin"] else ["Hello World"] end [200, { "content-type" => "text/plain" }, body] end run app
app = lambda do |env| request = Rack::Request.new(env) [200, { "content-type" => "text/plain" }, ["HTTP #{request.request_method}"]] end run app
Reading Request Bodies
If a body is submitted with the HTTP request, Rack provides access to it as an IO-like input stream object, via env["rack.input"] or Rack::Request#body.
The below application demonstrates how a POST request with a JSON body could be handled:
require "json" app = lambda do |env| body = if env["REQUEST_METHOD"] == "POST" raw_body = env["rack.input"].read city = JSON.parse(raw_body)['city'] ["Hello #{city}"] else ["Hello World!"] end [200, { "content-type" => "text/plain" }, body] end run app
Run the application and make a request:
$ curl -X "POST" "http://localhost:9292/" \
-H 'Content-Type: application/json; charset=utf-8' \
-d '{ "city": "London" }'
Hello London
Streaming Response Bodies
In addition to the body being an enumerable of strings, the body can also be a callable object, which is yielded a stream to write to:
app = lambda do |env| body = proc do |stream| 5.times do stream.write "#{Time.now}\n\n" sleep 1 end ensure stream.close end [200, { "content-type" => "text/plain" }, body] end run app
Run this application and then make a request to it:
$ curl localhost:9292
You’ll see the time is printed out 5 times at 1 second intervals and then the connection is closed.
Streaming bodies may make it easier for Rack applications to implement communication over persistent connections such as HTTP Server Sent Events and WebSockets.
Rack Middleware
Rack applications can be wrapped using middleware which may operate upon a request before it reaches the main application, and again after the application has returned a response to the request. Middleware is usually used for tasks like logging, caching, authentication, and measuring performance.
A Rack middleware must have a new method that accepts the Rack app and any arguments used to configure the middleware. The new method must return a Rack application that responds to call. Typically, Rack middleware are classes, and each instance of the middleware wraps access to the related application:
class MyMiddleware def initialize(app) @app = app end def call(env) # Operations before the request hits the main application # ------------------------------------------------------- # Propgate the request down the middleware stack status, headers, body = @app.call(env) # --------------------------------------- # Operations after the request comes back # Propogate the response up the middleware stack [status, headers, body] end end
Middleware can short-circuit the stack by skipping @app.call completely and returning a reponse by itself. This means the request never hits the main application or the remaining middleware in the stack. A middleware to authenticate a request might use this technique.
class AuthenticateRequest def initialize(app) @app = app end def call(env) if authenticated?(env["HTTP_AUTHORIZATION"]) @app.call(env) else [401, { "content-type" => "text/plain" }, ["Authentication failed"]] end end def authenticated?(token) # ... end end
Middleware is added to a Rack app with use:
class AuthenticateRequest # ... end app = lambda do |env| [200, { "content-type" => "text/plain" }, ["Hello World"]] end use AuthenticateRequest run app
This DSL to construct Rack applications is provided by Rack::Builder.
Rack ships with several pieces of middleware for common use-cases.
Conclusion
Since Rack provides a standardized interface between web servers and applications, in general, any Rack-compliant web application can run using any Rack-compliant web server. Rack uses a simple request (env) to response ([status, headers, body]) design, providing the foundation upon which the majority of Ruby web applications are built.
As Rack is designed to be low-level, Ruby web applications are typically developed using frameworks that build on top of Rack. Most frameworks offer access to the low-level Rack API.