Rack Interface

Master the Ruby web server interface and middleware. Learn how Rack powers Ruby web frameworks and build your own Rack applications.

Intermediate 1 min read

Rack Interface in Ruby

Introduction to Rack

Rack is a minimal, modular, and adaptable interface for developing web applications in Ruby. It provides a standard interface between web servers and Ruby web frameworks like Rails, Sinatra, and many others.

What is Rack?

# Rack is the foundation that powers Ruby web applications
# It defines a simple interface: an app that responds to #call
# and returns an array with [status, headers, body]

# Basic Rack app structure:
app = lambda do |env|
  [200, {'Content-Type' => 'text/plain'}, ['Hello World']]
end

Creating Your First Rack Application

A Rack application is any Ruby object that responds to the `call` method, takes one argument (the environment hash), and returns an array with three elements: status, headers, and body.

Simple Rack App

# app.rb
class HelloWorld
  def call(env)
    status = 200
    headers = {'Content-Type' => 'text/html'}
    body = ['<h1>Hello from Rack!</h1>']

    [status, headers, body]
  end
end

# Alternative: Using a proc
HelloWorldProc = proc do |env|
  [200, {'Content-Type' => 'text/plain'}, ['Hello from Proc!']]
end

config.ru - Rack Configuration File

# config.ru
require_relative 'app'

# Run the Rack application
run HelloWorld.new

# Or run the proc version
# run HelloWorldProc

Running the Rack App

# Install rack gem
gem install rack

# Run the application
rackup config.ru

# Or specify port
rackup -p 3000 config.ru

# Visit http://localhost:9292 (default port)

Understanding the Rack Environment

The environment hash contains all the information about the HTTP request, including headers, request method, path, and more.

Inspecting the Environment

# env_inspector.rb
class EnvironmentInspector
  def call(env)
    body = ["<h1>Rack Environment</h1>"]
    body << "<table border='1'>"
    body << "<tr><th>Key</th><th>Value</th></tr>"

    env.sort.each do |key, value|
      body << "<tr>"
      body << "<td>#{key}</td>"
      body << "<td>#{value.inspect}</td>"
      body << "</tr>"
    end

    body << "</table>"

    [200, {'Content-Type' => 'text/html'}, body]
  end
end

Common Environment Variables

class RequestInfo
  def call(env)
    request_method = env['REQUEST_METHOD']    # GET, POST, etc.
    path_info = env['PATH_INFO']              # /users/123
    query_string = env['QUERY_STRING']        # name=john&age=30
    content_type = env['CONTENT_TYPE']        # application/json
    content_length = env['CONTENT_LENGTH']    # 1024
    http_host = env['HTTP_HOST']              # localhost:3000
    user_agent = env['HTTP_USER_AGENT']       # Browser info

    body = [<<~HTML]
      <h1>Request Information</h1>
      <p><strong>Method:</strong> #{request_method}</p>
      <p><strong>Path:</strong> #{path_info}</p>
      <p><strong>Query:</strong> #{query_string}</p>
      <p><strong>Host:</strong> #{http_host}</p>
      <p><strong>User Agent:</strong> #{user_agent}</p>
    HTML

    [200, {'Content-Type' => 'text/html'}, body]
  end
end

Rack Middleware

Middleware is a powerful Rack concept that allows you to wrap applications with additional functionality like logging, authentication, caching, and more.

Creating Custom Middleware

# logger_middleware.rb
class LoggerMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    start_time = Time.now

    # Call the next app in the stack
    status, headers, body = @app.call(env)

    end_time = Time.now
    duration = ((end_time - start_time) * 1000).round(2)

    puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} - #{status} (#{duration}ms)"

    [status, headers, body]
  end
end

# authentication_middleware.rb
class AuthenticationMiddleware
  def initialize(app, username, password)
    @app = app
    @username = username
    @password = password
  end

  def call(env)
    auth_header = env['HTTP_AUTHORIZATION']

    if auth_header && valid_credentials?(auth_header)
      @app.call(env)
    else
      unauthorized_response
    end
  end

  private

  def valid_credentials?(auth_header)
    # Basic HTTP authentication
    encoded_credentials = auth_header.split(' ').last
    credentials = Base64.decode64(encoded_credentials)
    username, password = credentials.split(':')

    username == @username && password == @password
  end

  def unauthorized_response
    headers = {
      'Content-Type' => 'text/plain',
      'WWW-Authenticate' => 'Basic realm="Restricted Area"'
    }
    [401, headers, ['Unauthorized']]
  end
end

Using Middleware in config.ru

# config.ru
require_relative 'app'
require_relative 'logger_middleware'
require_relative 'authentication_middleware'

# Add middleware to the stack
use LoggerMiddleware
use AuthenticationMiddleware, 'admin', 'secret'

# Built-in Rack middleware
use Rack::ContentLength  # Automatically sets Content-Length header
use Rack::ShowExceptions # Shows detailed error pages in development

# Run the main application
run HelloWorld.new

Built-in Rack Middleware

Rack comes with many useful middleware components that you can use in your applications.

Common Rack Middleware

# config.ru with various middleware
use Rack::Deflater        # Gzip compression
use Rack::ETag            # Automatic ETag generation
use Rack::ConditionalGet  # Handles If-None-Match and If-Modified-Since
use Rack::Head            # Handles HEAD requests
use Rack::MethodOverride  # Allows _method parameter for PUT/DELETE
use Rack::Static, root: 'public', urls: ['/css', '/js', '/images']

# CORS middleware
use Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :put, :delete]
  end
end

# Session middleware
use Rack::Session::Cookie, secret: 'your-secret-key-here'

run MyApp.new

Static File Serving

# Serving static files with Rack::Static
use Rack::Static,
  urls: ['/css', '/js', '/images', '/favicon.ico'],
  root: 'public',
  index: 'index.html'

# Custom static file middleware
class StaticFileServer
  def initialize(app, options = {})
    @app = app
    @root = options[:root] || 'public'
    @urls = options[:urls] || []
  end

  def call(env)
    path = env['PATH_INFO']

    if static_file?(path)
      serve_static_file(path)
    else
      @app.call(env)
    end
  end

  private

  def static_file?(path)
    @urls.any? { |url| path.start_with?(url) }
  end

  def serve_static_file(path)
    full_path = File.join(@root, path)

    if File.exist?(full_path) && File.file?(full_path)
      content = File.read(full_path)
      content_type = determine_content_type(path)

      [200, {'Content-Type' => content_type}, [content]]
    else
      [404, {'Content-Type' => 'text/plain'}, ['File not found']]
    end
  end

  def determine_content_type(path)
    case File.extname(path)
    when '.html' then 'text/html'
    when '.css'  then 'text/css'
    when '.js'   then 'application/javascript'
    when '.png'  then 'image/png'
    when '.jpg', '.jpeg' then 'image/jpeg'
    else 'application/octet-stream'
    end
  end
end

Routing with Rack

While Rack doesn't include routing by default, you can easily implement basic routing functionality.

Simple Router Implementation

# router.rb
class Router
  def initialize
    @routes = {}
  end

  def add_route(method, path, app)
    @routes[[method, path]] = app
  end

  def call(env)
    method = env['REQUEST_METHOD']
    path = env['PATH_INFO']

    app = @routes[[method, path]]

    if app
      app.call(env)
    else
      not_found_response
    end
  end

  private

  def not_found_response
    [404, {'Content-Type' => 'text/html'}, ['<h1>404 Not Found</h1>']]
  end
end

# Usage in config.ru
router = Router.new

# Define routes
router.add_route('GET', '/', lambda { |env|
  [200, {'Content-Type' => 'text/html'}, ['<h1>Home Page</h1>']]
})

router.add_route('GET', '/about', lambda { |env|
  [200, {'Content-Type' => 'text/html'}, ['<h1>About Us</h1>']]
})

router.add_route('POST', '/users', lambda { |env|
  [201, {'Content-Type' => 'application/json'}, ['{"message": "User created"}']]
})

run router

Advanced Router with Parameters

class AdvancedRouter
  def initialize
    @routes = []
  end

  def add_route(method, pattern, &block)
    @routes << {
      method: method,
      pattern: compile_pattern(pattern),
      handler: block
    }
  end

  def call(env)
    method = env['REQUEST_METHOD']
    path = env['PATH_INFO']

    route = find_route(method, path)

    if route
      # Extract parameters from the path
      params = extract_params(route[:pattern], path)
      env['rack.route_params'] = params

      route[:handler].call(env)
    else
      not_found_response
    end
  end

  private

  def compile_pattern(pattern)
    # Convert /users/:id to regex
    regex_pattern = pattern.gsub(/:(\w+)/, '(?<\\1>[^/]+)')
    /\A#{regex_pattern}\z/
  end

  def find_route(method, path)
    @routes.find do |route|
      route[:method] == method && route[:pattern].match(path)
    end
  end

  def extract_params(pattern, path)
    match = pattern.match(path)
    match ? match.named_captures : {}
  end

  def not_found_response
    [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
  end
end

# Usage
router = AdvancedRouter.new

router.add_route('GET', '/users/:id') do |env|
  user_id = env['rack.route_params']['id']
  [200, {'Content-Type' => 'application/json'}, ["{\"id\": \"#{user_id}\"}"]]
end

router.add_route('GET', '/posts/:post_id/comments/:id') do |env|
  params = env['rack.route_params']
  post_id = params['post_id']
  comment_id = params['id']

  body = "{\"post_id\": \"#{post_id}\", \"comment_id\": \"#{comment_id}\"}"
  [200, {'Content-Type' => 'application/json'}, [body]]
end

run router

Building a Complete Rack Application

Let's build a more comprehensive Rack application that demonstrates multiple concepts working together.

Blog Application with Rack

# blog_app.rb
require 'json'
require 'erb'

class BlogApp
  def initialize
    @posts = [
      { id: 1, title: 'First Post', content: 'This is my first blog post!', created_at: Time.now - 86400 },
      { id: 2, title: 'Learning Rack', content: 'Rack is awesome for web development.', created_at: Time.now }
    ]
  end

  def call(env)
    request = Rack::Request.new(env)

    case [request.request_method, request.path_info]
    when ['GET', '/']
      home_page(request)
    when ['GET', '/posts']
      list_posts(request)
    when ['GET', %r{^/posts/(\d+)$}]
      show_post(request, $1.to_i)
    when ['POST', '/posts']
      create_post(request)
    when ['GET', '/api/posts']
      api_list_posts(request)
    else
      not_found
    end
  end

  private

  def home_page(request)
    html = <<~HTML
      <!DOCTYPE html>
      <html>
      <head>
        <title>My Rack Blog</title>
        <style>
          body { font-family: Arial, sans-serif; margin: 40px; }
          .post { border: 1px solid #ccc; padding: 20px; margin: 20px 0; }
          .nav { margin-bottom: 30px; }
          .nav a { margin-right: 20px; text-decoration: none; }
        </style>
      </head>
      <body>
        <div class="nav">
          <a href="/">Home</a>
          <a href="/posts">All Posts</a>
          <a href="/api/posts">API</a>
        </div>
        <h1>Welcome to My Rack Blog</h1>
        <p>This is a simple blog built with Rack!</p>

        <h2>Recent Posts</h2>
        #{@posts.last(3).map { |post| render_post_summary(post) }.join}

        <h2>Create New Post</h2>
        <form method="POST" action="/posts">
          <p>
            <label>Title:</label><br>
            <input type="text" name="title" required style="width: 300px;">
          </p>
          <p>
            <label>Content:</label><br>
            <textarea name="content" required style="width: 300px; height: 100px;"></textarea>
          </p>
          <p>
            <input type="submit" value="Create Post">
          </p>
        </form>
      </body>
      </html>
    HTML

    [200, {'Content-Type' => 'text/html'}, [html]]
  end

  def list_posts(request)
    html = <<~HTML
      <!DOCTYPE html>
      <html>
      <head><title>All Posts</title></head>
      <body>
        <h1>All Blog Posts</h1>
        #{@posts.map { |post| render_post_summary(post) }.join}
        <p><a href="/">← Back to Home</a></p>
      </body>
      </html>
    HTML

    [200, {'Content-Type' => 'text/html'}, [html]]
  end

  def show_post(request, post_id)
    post = @posts.find { |p| p[:id] == post_id }

    if post
      html = <<~HTML
        <!DOCTYPE html>
        <html>
        <head><title>#{post[:title]}</title></head>
        <body>
          <h1>#{post[:title]}</h1>
          <p><small>Posted on #{post[:created_at].strftime('%Y-%m-%d %H:%M')}</small></p>
          <div>#{post[:content]}</div>
          <p><a href="/posts">← Back to All Posts</a></p>
        </body>
        </html>
      HTML

      [200, {'Content-Type' => 'text/html'}, [html]]
    else
      not_found
    end
  end

  def create_post(request)
    title = request.params['title']
    content = request.params['content']

    if title && content && !title.empty? && !content.empty?
      new_id = (@posts.map { |p| p[:id] }.max || 0) + 1
      new_post = {
        id: new_id,
        title: title,
        content: content,
        created_at: Time.now
      }

      @posts << new_post

      # Redirect to the new post
      [302, {'Location' => "/posts/#{new_id}"}, []]
    else
      [400, {'Content-Type' => 'text/html'}, ['<h1>400 Bad Request</h1><p>Title and content are required.</p>']]
    end
  end

  def api_list_posts(request)
    posts_json = @posts.map do |post|
      {
        id: post[:id],
        title: post[:title],
        content: post[:content],
        created_at: post[:created_at].iso8601,
        url: "/posts/#{post[:id]}"
      }
    end

    [200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(posts_json)]]
  end

  def render_post_summary(post)
    <<~HTML
      <div class="post">
        <h3><a href="/posts/#{post[:id]}">#{post[:title]}</a></h3>
        <p>#{post[:content][0..100]}#{post[:content].length > 100 ? '...' : ''}</p>
        <p><small>Posted on #{post[:created_at].strftime('%Y-%m-%d %H:%M')}</small></p>
      </div>
    HTML
  end

  def not_found
    [404, {'Content-Type' => 'text/html'}, ['<h1>404 Not Found</h1>']]
  end
end

config.ru for Blog App

# config.ru
require_relative 'blog_app'

# Add middleware
use Rack::ShowExceptions
use Rack::MethodOverride
use Rack::ContentLength

# Handle static files
use Rack::Static,
  urls: ['/css', '/js', '/images'],
  root: 'public'

# Run the blog application
run BlogApp.new

Testing Rack Applications

Testing Rack applications is straightforward using the rack-test gem and standard testing frameworks.

Testing with Minitest

# test_blog_app.rb
require 'minitest/autorun'
require 'rack/test'
require_relative 'blog_app'

class BlogAppTest < Minitest::Test
  include Rack::Test::Methods

  def app
    BlogApp.new
  end

  def test_home_page
    get '/'

    assert_equal 200, last_response.status
    assert_includes last_response.body, 'Welcome to My Rack Blog'
    assert_includes last_response.body, 'form'
  end

  def test_api_posts
    get '/api/posts'

    assert_equal 200, last_response.status
    assert_equal 'application/json', last_response.content_type

    posts = JSON.parse(last_response.body)
    assert posts.is_a?(Array)
    assert posts.length > 0
  end

  def test_create_post
    post '/posts', { title: 'Test Post', content: 'Test content' }

    assert_equal 302, last_response.status
    assert_match %r{/posts/\d+}, last_response.location
  end

  def test_show_post
    get '/posts/1'

    assert_equal 200, last_response.status
    assert_includes last_response.body, 'First Post'
  end

  def test_not_found
    get '/nonexistent'

    assert_equal 404, last_response.status
  end
end

Rack Best Practices

  • Keep middleware lightweight and focused on a single responsibility
  • Use Rack::Request and Rack::Response for easier request/response handling
  • Always return proper HTTP status codes
  • Set appropriate Content-Type headers
  • Handle errors gracefully with proper error responses
  • Use middleware for cross-cutting concerns (logging, authentication, etc.)
  • Test your Rack applications thoroughly using rack-test
  • Consider using existing Rack middleware before writing custom solutions
  • Keep the body enumerable - use arrays or objects that respond to #each
  • Close resources properly in middleware (use ensure blocks)