Ruby Logo

Rack Interface

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

Home Ruby Rack Interface

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)

Quick Navigation

Related Topics

Video Tutorial

Watch and learn rack interface

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!