Setting the backstage: Rack and Sinatra

Building Web Applications with Ruby, Rack and Sinatra

Welcome to this practical guide on how to build web applications using Ruby, Rack, and Sinatra — complete with automated testing using RSpec. This post is designed to walk you through the concepts and tools step-by-step with hands-on examples and code.

We’ll explore:

  • Ruby’s ecosystem for web development (Before Rails takes the scene)
  • Building minimal web apps using Rack and Sinatra
  • Rendering HTML responses with template engines
  • Testing applications with RSpec

🌐 Remembering Web Development Fundamentals

Before diving into coding, let’s remember two important concepts regarding client-server communication:

  • Client: Browser or external application making the request.
  • Server: Application processing the request and returning a response.

Since we are in the backend "path" we’ll be focusing more on the code that will run on the server side of things, "behind the scenes". It’s the backend’s job to handle user requests, manage data, communicate with databases (and other services in the infraestructure layer) and much more.

🔌 Rack – The Ruby Webserver Interface

Rack is a minimal interface between web servers and Ruby frameworks.

Highlights:

  • Launched in 2007 and currently in version 3.0.9.1 (at the time of writing).
  • Acts as middleware for frameworks like Sinatra and Rails.
  • To use rack you need to create a callable "app" object. It receives and interprets the entire request and env as a hash. It should return an array with:
    1. HTTP status code
    2. Headers hash
    3. Response body (This should respond to the #each method)

Installing Rack

mkdir my_trainee_app && cd my_trainee_app
bundle init
bundle add webrick -v 1.8.1
bundle add rack -v 3.0.8
bundle add rackup -v 2.1.0

Basic Example with Rack

app.rb:

require 'rack'
require 'rackup'

app = Proc.new do |env|
  [200, { 'content-type' => 'text/html' }, ["My rack hello world.\n"]]
end

Rackup::Handler::WEBrick.run app

Run with:

bundle exec ruby app.rb
# Visit http://localhost:8080

Using rackup with config.ru

Alternatively to the method above, we can use rackup to run our application. The rackup utility requires a config.ru file like the one below:

# config.ru
app = Proc.new do |env|
  [200, {'content-type' => 'text/html'}, ["My rackup hello world.\n"]]
end

run app

Run with:

bundle exec rackup
# Visit http://localhost:9292

Notice that, we are simply returning a predefined answer from this simple application. To make it actually behave like a server, complete with routes and other amenities, we’d need to manually implement all of this logic, which would be terrible to do and maintain. That’s the reason we have frameworks (like Sinatra, Hanami and Rails) that encapsulate and abstract all of that to us.

One thing to notice is that rackup is considered to be "soft deprecated". There are currently other alternatives to it and it should not be directly used nowadays.
Curiously, most web frameworks still have some kind of dependency on it.

🎙️ Sinatra – The Lightweight Ruby Framework

Sinatra simplifies building Ruby web applications. It has a simple and intuitive interface to define routes and access information from HTTP requests, as well as a simple interface to build responses.

Highlights:

  • Created in 2007, current version 4.1.1 (at the time of writing)
  • Minimal dependencies, ideal for APIs and microservices
  • Built on top of Rack

Installing Sinatra

bundle add sinatra -v 4.0.0
bundle install

Basic Sinatra App

Replace app.rb with:

require 'sinatra'

get '/' do
  "My sinatra hello world.\n"
end

Run it:

bundle exec ruby app.rb
# Visit http://localhost:4567

Sinatra with rackup

app.rb:

require 'sinatra'

class App < Sinatra::Base
  get '/' do
    "My sinatra rackup hello world.\n"
  end
end

config.ru:

require_relative 'app'
run App

Notice that, differently from using rack directly, Sinatra offers many useful abstractions that make developing a web app much simpler and easier to maintain. We’ll explore this a bit more later.

📄 Rendering Formats

We are quite used to navigating the web, and you are probably familiar with some of the most common formats available. One important thing to understand about this is that, in the end, what our application is doing is merely serving some kind of file that will be interpreted by the client side in some way. If we return an HTML (+ CSS and JS), the browser will then process that in the form of the beautiful pages we see online. But more than HTML, we can return lots of other stuff, that different clients will deal with in different ways. Maybe a binary like an image, a video or PDF file, or even an excel spreadsheet. Maybe another text-based content like a JSON or XML. You can return pretty much anything. The kind of file you are either consuming or delivering is often called a MIME Type. You can check a list of the most common ones here.

For us, however, the types we’ll focus on and are most interested in discussing are those that are particularly important for web development. They’d be text types (like HTML, Javascript, and CSS) and application types (like JSON). To generate those files, you will often need to use template engines and builders to help you construct them.

Template Engines:

  • ERB (Embeded Ruby, used to build pretty much any kind of template)
  • Haml, Slim (more concise syntax)
  • Liquid, Mustache (logic-less templates)

Other tools like Nokogiri, and jbuilder offer many useful tools to build safe and easy-to-maintain templates.
For now, we’ll be focusing on ERB. With it, you can insert Ruby code inside any text file by using special delimiters like <% %> for Ruby code execution and <%= %> to output results to the HTML.

Example – ERB:

<h1>Welcome</h1>
<% if @user.logged_in? %>
  <p>Hello, <%= @user.name %>!</p>
<% else %>
  <p>Please log in to continue.</p>
<% end %>

🏆 Challenge: Building a Sinatra-based Calculator

In this section, we’ll apply all the knowledge acquired so far by implementing a simple calculator web app using Sinatra. The application will support basic operations: addition, subtraction, multiplication, and division. Let’s not forget to add tests.

Objective

Build a Sinatra application that simulates a calculator capable of handling basic arithmetic operations via a web form. The results should be displayed dynamically and covered by automated tests.

File Structure

my_calculator_app/
├── app.rb
├── config.ru
├── Gemfile
├── Gemfile.lock
├── models
│   └── calculator.rb
├── spec
│   ├── features
│   │   └── app_spec.rb
│   ├── models
│   │   └── calculator_spec.rb
│   └── spec_helper.rb
└── views
    ├── index.erb
    └── result.erb

Gemfile

source 'https://rubygems.org'

gem 'sinatra'
gem 'rackup'
gem 'puma' # => Puma is the webserver rackup will use. It could also be 'falcon' or 'webrick'
gem 'rspec'
gem 'rack-test'
gem 'simplecov'

models/calculator.rb

class Calculator
  def calculate(a, b, operation)
    a = a.to_f
    b = b.to_f

    case operation
    when '+' then a + b
    when '-' then a - b
    when '*' then a * b
    when '/' then b.zero? ? 'Division by zero' : a / b
    else 'Invalid operation'
    end
  end
end

app.rb

require 'sinatra'
require 'erb'
require_relative 'models/calculator'

class App < Sinatra::Base
  get '/' do
    erb :index
  end

  post '/calculate' do
    @a = params[:a]
    @b = params[:b]
    @operation = params[:operation]
    calculator = Calculator.new
    @result = calculator.calculate(@a, @b, @operation)
    erb :result
  end
end

views/index.erb

<h1>Sinatra Calculator</h1>
<form action="/calculate" method="post">
  <input type="text" name="a" placeholder="First number" required>
  <select name="operation">
    <option value="+">+</option>
    <option value="-">-</option>
    <option value="*">*</option>
    <option value="/">/</option>
  </select>
  <input type="text" name="b" placeholder="Second number" required>
  <input type="submit" value="Calculate">
</form>

views/result.erb

<h1>Result</h1>
<p><%= @a %> <%= @operation %> <%= @b %> = <%= @result %></p>
<a href="/">Try another calculation</a>

config.ru

require_relative 'app'
run App

spec/spec_helper.rb

ENV['RACK_ENV'] = 'test'
require 'simplecov'
SimpleCov.start

require_relative '../app'
require 'rspec'
require 'rack/test'

RSpec.configure do |config|
  config.include Rack::Test::Methods
end

spec/models/calculator_spec.rb

require_relative '../../models/calculator'
require_relative '../../spec/spec_helper'

describe Calculator do
  let(:calculator) { Calculator.new }

  it 'adds numbers correctly' do
    expect(calculator.calculate(2, 3, '+')).to eq(5)
  end

  it 'subtracts numbers correctly' do
    expect(calculator.calculate(5, 3, '-')).to eq(2)
  end

  it 'multiplies numbers correctly' do
    expect(calculator.calculate(4, 3, '*')).to eq(12)
  end

  it 'divides numbers correctly' do
    expect(calculator.calculate(10, 2, '/')).to eq(5)
  end

  it 'handles division by zero' do
    expect(calculator.calculate(10, 0, '/')).to eq('Division by zero')
  end

  it 'returns invalid for unknown operations' do
    expect(calculator.calculate(2, 2, '^')).to eq('Invalid operation')
  end
end

spec/features/app_spec.rb

require_relative '../../spec/spec_helper'

RSpec.describe 'Calculator App' do
  def app
    App
  end

  it 'loads the homepage' do
    get '/'
    expect(last_response).to be_ok
    expect(last_response.body).to include('Sinatra Calculator')
  end

  it 'performs an addition' do
    post '/calculate', a: '3', b: '5', operation: '+'
    expect(last_response.body).to include('3 + 5 = 8.0')
  end

  it 'performs a multiplication' do
    post '/calculate', a: '3', b: '5', operation: '*'
    expect(last_response.body).to include('3 * 5 = 15.0')
  end

  it 'performs a subtraction' do
    post '/calculate', a: '3', b: '5', operation: '-'
    expect(last_response.body).to include('3 - 5 = -2.0')
  end

  it 'performs a division' do
    post '/calculate', a: '3', b: '5', operation: '/'
    expect(last_response.body).to include('3 / 5 = 0.6')
  end
end

Running the Application

Start the app using Rack:

$ bundle exec rackup

Visit http://localhost:9292 in your browser.

Running the Tests

$ bundle exec rspec

You should see all tests pass, with a coverage report generated by SimpleCov with 100% coverage.

Conclusion

And that’s it. In this post, you learned how to build and test a simple web app using Sinatra. Although simple, Sinatra is a great tool to build less complex applications and allows us to dig into a "lower-level-ish" framework before experiencing something more complex like Rails. Take your time to explore the tools presented here and get comfortable with all these concepts!

Previously: Ruby for the Curious: A Hands-On Guide to Getting Started

This post is part of our ‘The Miners’ Guide to Code Crafting’ series, designed to help aspiring developers learn and grow. Stay tuned for more and continue your coding journey with us!! Check out the full summary here!

🔗 Resources

We want to work with you. Check out our "What We Do" section!