Rack for Ruby: The Complete Beginner’s Guide

David Morales David Morales
/
Futuristic metal shelf with glowing red rubies connected to industrial machines with cables in a dark laboratory

Rack acts as a middle layer between a Ruby web server and a Ruby application. In fact, Rack is the foundation for every major Ruby web framework, such as Rails or Sinatra, which makes it a critical part of the Ruby ecosystem.

Let’s explore how Rack lets you spin up a local server in just a few lines and understand how HTTP requests are handled.

What is Rack?

Rack is a minimal interface for exposing a Ruby application to the Internet. It defines how this application should communicate with a Ruby web server (such as Puma or Unicorn).

The key features of Rack include:

“Hello World” with Rack

Let’s create a basic example to understand how Rack works. We will use Rack::Builder’s DSL, often referred to as the rackup pattern.

  1. Create a directory, for example: rack-demo:

    Terminal window
    mkdir rack-demo
    cd rack-demo
  2. Generate a Gemfile and add the necessary dependencies:

    Terminal window
    bundle init
    bundle add rack rackup puma
  3. Create the Rack configuration file config.ru with the following content:

    config.ru
    class HelloWorld
    def call(env)
    [200, { "content-type" => "text/plain" }, ["Hello world!"]]
    end
    end
    run HelloWorld.new

    First, define the class that represents the application. Then use Rack’s run method, passing the instance as an argument.

  4. Start the web server:

    Terminal window
    bundle exec rackup

    You should see output like:

    Puma starting in single mode...
    * Puma version: 6.6.0 ("Return to Forever")
    * Ruby version: ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin24]
    * Min threads: 0
    * Max threads: 5
    * Environment: development
    * PID: 80042
    * Listening on http://127.0.0.1:9292
    * Listening on http://[::1]:9292
    Use Ctrl-C to stop

    Rack is using Puma on port 9292.

    When you run rackup, it automatically loads the config.ru file and requires rack, which is why the run method from Rack::Builder is available.

  5. Make a request to the server using curl or a browser:

    Terminal window
    curl localhost:9292

    You will see the message rendered. No frameworks, no magic involved.

Anatomy of the call Method

Let’s look at the structure of the call method in more detail:

def call(env)
status = 200
headers = { "content-type" => "text/plain" }
body = ["Hello world!"]
[status, headers, body]
end
All of this is specified by the Rack specification.

Simulating Streaming

As mentioned earlier, the response body must be an object that includes Enumerable. If the body is HTML content and you send chunks with delays, the browser will render them as they arrive, creating a streaming effect.

Try the following:

class Application
def call(env)
status = 200
headers = { "content-type" => "text/html" }
body = Enumerator.new do |output|
output << "<h1>Some title</h1>\n"
sleep 1
output << "<p>First paragraph.</p>\n"
sleep 1
output << "<p>Second paragraph.</p>"
end
[status, headers, body]
end
end
run Application.new

You will see each section appear gradually.

Using a Lambda

Since call is the only required method, you can use lambdas or procs as Rack applications. Here is a simplified example:

config.ru
application = -> (env) do
status = 200
headers = { "content-type" => "text/plain" }
body = ["Hello world!"]
[status, headers, body]
end
run application

Implementing a Simple Router

The env hash exposes both the HTTP method and the path.

KeyWhat it contains
REQUEST_METHODHTTP verb (GET, POST, PUT…)
PATH_INFOPart of the URL after the domain

This allows us to build a simple router:

config.ru
class Application
def call(env)
method = env['REQUEST_METHOD']
path = env['PATH_INFO']
return method_not_allowed(method) unless method == 'GET'
case path
when '/'
ok("<h1>Main page</h1>")
when '/about'
ok("<h1>About</h1>")
else
not_found
end
end
private
def ok(body)
[200, {"content-type"=>"text/html"}, [body]]
end
def not_found
[404, {"content-type"=>"text/plain"}, ["The page does not exist"]]
end
def method_not_allowed(method)
[405, {"content-type"=>"text/plain"}, ["Unsupported method: #{method}"]]
end
end
run Application.new

Try the following URLs in your browser:

You can also make a POST request using curl:

Terminal window
curl -i -X POST http://localhost:9292

This will return:

HTTP/1.1 405 Method Not Allowed
content-type: text/plain
Content-Length: 24
Unsupported method: POST

Imagine rendering an ERB template for each route (as shown in this article about Embedding Ruby in HTML). You would be getting very close to how a microframework like Sinatra works.

Exercise

Create a Rack app that:

  1. Responds with a JSON object like {"time":"HH:MM:SS"} when you send a GET request to the /time path.
  2. Returns a 404 error for any other path.
Hint

You will need to set the content type to application/json and require the json library.

Solution
config.ru
require "json"
class Application
def call(env)
if env["REQUEST_METHOD"] == "GET" && env["PATH_INFO"] == "/time"
body = { time: Time.now.strftime("%H:%M:%S") }.to_json
[200, {"content-type" => "application/json"}, [body]]
else
[404, {"content-type"=>"text/plain"}, ["Not Found"]]
end
end
end
run Application.new

Middleware: Rack’s Superpower

A middleware is a small Rack object that sits between the web server and your application. It receives the request, can modify it, and then optionally adjust the response as well.

Rack allows you to chain multiple middleware components together. Each one processes the request and passes it along until the final response is returned.

Here are some popular Rack middleware gems:

GemPurpose
rack-staticServe static files
rack-deflaterGzip compression
rack-sessionSession management via cookies
rack-cacheHTTP caching
rack-corsCORS support

To illustrate how middleware works, I will show you examples using a few of them.

Example with rack-static

The rack-static gem lets you serve static files.

Example with rack-deflater

This gem compresses the response body using gzip.

Example with rack-session

This gem enables session storage in cookies. Here is how you can track user visits.

Restart Rack and visit the page in your browser. You will see that the total number of visits is 1. If you reload the page, it will increase to 2, and so on. Open the Developer Tools, and you will find the cookie under Storage.

Creating Your Own Middleware

After reviewing examples of existing middleware, let’s now see how to create one from scratch.

Unlike a regular Rack app, a middleware must be defined as a class because it receives the next app in its constructor:

config.ru
class MiddleTimer
def initialize(app) = @app = app
def call(env)
start = Time.now
status, headers, body = @app.call(env)
duration = ((Time.now - start) * 1000).round
duration_message = "\n[#{env['PATH_INFO']}] took #{duration}ms"
[status, headers, body << duration_message]
end
end
use MiddleTimer
run -> (env) do
sleep(rand(0.1..0.5)) # Simulate some processing time
[200, { 'content-type' => 'text/plain' }, ["A random sleep was performed."]]
end

Open the browser and you will see the text along with the time it took to process the request.

Chaining Middleware

Here is how you can chain two middleware components: one that adds a prefix to the response body, and one that adds a suffix.

config.ru
class PrefixingMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body = ["<p>This is a prefix</p>"] + body
[status, headers, body]
end
end
class PostfixingMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body += ["<p>This is a postfix</p>"]
[status, headers, body]
end
end
use PrefixingMiddleware
use PostfixingMiddleware
run -> (env) {
[200, {"content-type" => "text/html"}, ["<p>This is a Rack app</p>"]]
}

When the file is executed, Rack builds the middleware chain from bottom to top:

  1. It starts with the run statement and executes the lambda.
  2. Then, the code moves upward, instantiating PostfixingMiddleware and passing the Rack application (the lambda we defined) as an argument.
  3. It continues to the next middleware, instantiating PrefixingMiddleware and passing it an instance of PostfixingMiddleware.

At this point, the chain is fully constructed and ready for execution. The request will begin at the first element in the chain (PrefixingMiddleware), which calls its call method with the web request (captured in the env variable).

The first thing the call method does is call the call method of the next component, which is PostfixingMiddleware. That in turn calls the application (the lambda), which simply returns the array.

Execution then moves back up the chain: the array is returned from the lambda to PostfixingMiddleware, which modifies the body and returns the array. This is then received by PrefixingMiddleware, which also modifies the response and returns the final array.

This completes the chain.

The result is a response that includes the prefix, main content, and suffix in that order.

Conclusion

Rack is a minimal interface that sits between a Ruby application (which could be a framework like Ruby on Rails) and a Ruby web server. This setup allows any Rack-compliant application to work with different web servers without changing its code.

Rack defines:

  1. A structure for the request (a hash with many environment keys and values).
  2. A structure for the response (an array with status, headers, and body).
  3. A common interface between applications and web servers.

Rack also supports modular components called middleware, which can be chained together to build up functionality in a clean and reusable way.

Test your knowledge

  1. Which single method must any object implement to be a Rack application?

  1. What three elements must the call method return?

  1. Which of the following objects meets Rack’s requirements for the body part of the response?

  1. Because Rack only requires a call method, which of the following can itself be the entire Rack application?

  1. In what order does Rack build the middleware chain declared in config.ru?