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:
- HTTP status code
- Headers hash
- 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
- MDN Client-Server Overview
- Sinatra Documentation
- HTTP Methods
- RSpec Docs
- Better Specs
- Repo for the example Sinatra app
We want to work with you. Check out our "What We Do" section!