In this article, we'll introduce Ruby on Rails' lesser-known but powerful cousin Sinatra. We'll use the framework to build a cost-of-living calculator app.
By the end of the article, you'll know what Sinatra is and how to use it.
Let's go!
Our Scenario
Imagine this: you've just landed a job as a Ruby developer for a growing startup and your new boss has agreed to let you work remotely for as long as you like.
You start dreaming of all the cool cities where you could move to begin your digital-nomad life. You want to go somewhere nice but, most importantly, affordable. And to help you decide, you hit upon an idea to build a small app that shows cost-of-living data for almost any city or country you enter.
With so many languages, frameworks, and no-code tools available today, what will you use to go from idea to app?
Enter Sinatra!
Overview of Sinatra
Compared to Ruby on Rails, a full-stack web framework, Sinatra is a very lean micro-framework originally developed by Blake Mizerany to help Ruby developers build applications with "minimal effort".
With Sinatra, there is no Model-View-Controller (MVC) pattern, nor does it encourage you to use "convention over configuration" principles. Instead, you get a flexible tool to build simple, fast Ruby applications.
What Is Sinatra Good For?
Because of its lightweight and Rack-based architecture, Sinatra is great for building APIs, mountable app engines, command-line tools, and simple apps like the one we'll build in this tutorial.
Our Example Ruby App
The app we are building will let you input how much you earn as well as the city and country you'd like to move to. Then it will output a few living expense figures for that city.
Prerequisites
To follow along, ensure you have the following:
- Ruby development environment (at least version 3.0.0+) already set up.
- Bundler and Sinatra installed on your development environment. If you don't have Sinatra, simply run
gem install Sinatra
. - A free RapidAPI account since we'll use one of their APIs for our app project.
You can also get the full code for the example app here.
Before proceeding with our build, let's discuss something very important: the structure of Sinatra apps.
Regular (Classical) Vs. Modular Sinatra Apps
When it comes to structure in Sinatra apps, you can have regular — sometimes referred to as "classical" — apps, or "modular" ones.
In a classical Sinatra app, all your code lives in one file. You'll almost always find that you can only run one Sinatra app per Ruby process if you choose the regular app structure.
The example below shows a simple classical Sinatra app.
# main.rb
require 'sinatra'
require 'json'
get '/' do
# here we specify the content type to respond with
content_type :json
{ item: 'Red Dead Redemption 2', price: 19.79, status: 'Available' }.to_json
end
This one file contains everything needed for this simplified app to run. Run it with ruby main.rb
, which should spin up an instance of the Thin web server (the default web server that comes with Sinatra). Visit localhost:4567
and you'll see the JSON response.
As you can see, it is relatively easy to extend this simple example into a fairly-complex API app with everything contained in one file (the most prominent feature of the classical structure).
Now let's turn our attention to modular apps.
The code below shows a basic modular Sinatra app. At first glance, it looks pretty similar to the classic app we've already looked at — apart from a rather simple distinction. In modular apps, we subclass Sinatra::Base
, and each "app" is defined within this subclassed scope.
# main.rb
require 'sinatra/base'
require 'json'
require_relative 'lib/fetch_game_data'
# main module/class defined here
class GameStoreApp < Sinatra::Base
get '/' do
content_type :json
{ item: 'Red Dead Redemption 2', price: 19.79, status: 'Available' }.to_json
end
not_found do
content_type :json
{ status: 404, message: "Nothing Found!" }.to_json
end
end
Have a look at the Sinatra documentation in case you need more information on this.
Let's now continue with our app build.
Structuring Our Ruby App
To begin with, we'll take the modular approach with this build so it's easy to organize functionality in a clean and intuitive way.
Our cost-of-living calculator app needs:
- A root page, which will act as our landing page.
- Another page with a form where a user can input their salary information.
- Finally, a results page that displays some living expenses for the chosen city.
The app will fetch cost-of-living data from an API hosted on RapidAPI.
We won't include any tests or user authentication to keep this tutorial brief.
Go ahead and create a folder structure like the one shown below:
.
├── app.rb
├── config
│ └── database.yml
├── config.ru
├── db
│ └── development.sqlite3
├── .env
├── Gemfile
├── Gemfile.lock
├── .gitignore
├── lib
│ └── user.rb
├── public
│ └── css
│ ├── bulma.min.css
│ └── style.css
├── Rakefile
├── README.md
├── views
│ ├── index.erb
│ ├── layout.erb
│ ├── navbar.erb
│ ├── results.erb
│ └── start.erb
Here's what each part does in a nutshell (we'll dig into the details as we proceed with the app build):
-
app.rb
- This is the main file in our modular app. In here, we define the app's functionality. -
Gemfile
- Just like the Gemfile in a Rails app, you define your app's gem dependencies in this file. -
Rakefile
- Rake task definitions are defined here. -
config.ru
- For modular Sinatra apps, you need a Rack configuration file that defines how your app will run. - Views folder - Your app's layout and view files go into this folder.
- Public folder - Files that don't change much — such as stylesheets, images, and Javascript files — are best kept here.
- Lib folder - In here, you can have model files and things like specialized helper files.
- DB folder - Database migration files and the
seeds.rb
will go in here. - Config folder - Different configurations can go into this folder: for example, database settings.
The Main File (app.rb
)
app.rb
is the main entry point into our app where we define what the app does. Notice how we've subclassed Sinatra::Base
to make the app modular.
As you can see below, we include some settings for fetching folders as well as defining the public folder (for storing static files). Another important note here is that we register the Sinatra::ActiveRecordExtension
which lets us work with ActiveRecord as the ORM.
# app.rb
# Include all the gems listed in Gemfile
require 'bundler'
Bundler.require
module LivingCostCalc
class App < Sinatra::Base
# global settings
configure do
set :root, File.dirname(__FILE__)
set :public_folder, 'public'
register Sinatra::ActiveRecordExtension
end
# development settings
configure :development do
# this allows us to refresh the app on the browser without needing to restart the web server
register Sinatra::Reloader
end
end
end
Then we define the routes we need:
- The root, which is just a simple landing page.
- A "Start here" page with a form where a user inputs the necessary information.
- A results page.
# app.rb
class App < Sinatra::Base
...
# root route
get '/' do
erb :index
end
# start here (where the user enters their info)
get '/start' do
erb :start
end
# results
get '/results' do
erb :results
end
...
end
You might notice that each route includes the line erb :<route>
, which is how you tell Sinatra the respective view file to render from the "views" folder.
Database Setup for the Sinatra App
The database setup for our Sinatra app consists of the following:
- A database config file —
database.yml
— where we define the database settings for the development, production, and test databases. - Database adapter and ORM gems included in the Gemfile. We are using ActiveRecord for our app. Datamapper is another option you could use.
- Registering the ORM extension and the database config file in
app.rb
.
Here's the database config file:
# config/database.yml
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
test:
<<: *default
database: db/test.sqlite3
production:
adapter: postgresql
encoding: unicode
pool: 5
host: <%= ENV['DATABASE_HOST'] || 'db' %>
database: <%= ENV['DATABASE_NAME'] || 'sinatra' %>
username: <%= ENV['DATABASE_USER'] || 'sinatra' %>
password: <%= ENV['DATABASE_PASSWORD'] || 'sinatra' %>
And the ORM and database adaptor gems in the Gemfile:
# Gemfile
source "https://rubygems.org"
# Ruby version
ruby "3.0.4"
gem 'sinatra'
gem 'activerecord'
gem 'sinatra-activerecord' # ORM gem
gem 'sinatra-contrib'
gem 'thin'
gem 'rake'
gem 'faraday'
group :development do
gem 'sqlite3' # Development database adaptor gem
gem 'tux' # gives you access to an interactive console similar to 'rails console'
gem 'dotenv'
end
group :production do
gem 'pg' # Production database adaptor gem
end
And here's how you register the ORM and database config in app.rb
.
# app.rb
module LivingCostCalc
class App < Sinatra::Base
# global settings
configure do
...
register Sinatra::ActiveRecordExtension
end
# database settings
set :database_file, 'config/database.yml'
...
end
end
Connecting to the Cost-of-Living API
For our app to show relevant cost-of-living data for whatever city a user inputs, we have to fetch it via an API call to this API. Create a free RapidAPI account to access it if you haven't done so.
We'll make the API call using the Faraday gem. Add it to the Gemfile and run bundle install
.
# Gemfile
gem 'faraday'
With that done, we now include the API call logic in the results
method.
# app.rb
...
get '/results' do
city = params[:city]
country = params[:country]
# if country or city names have spaces, process accordingly
esc_city = ERB::Util.url_encode(country) # e.g. "St Louis" becomes 'St%20Louis'
esc_country = ERB::Util.url_encode(country) # e.g. "United States" becomes 'United%20States'
url = URI("https://cost-of-living-prices-by-city-country.p.rapidapi.com/get-city?city=#{esc_city}&country=#{esc_country}")
conn = Faraday.new(
url: url,
headers: {
'X-RapidAPI-Key' => ENV['RapidAPIKey'],
'X-RapidAPI-Host' => ENV['RapidAPIHost']
}
)
response = conn.get
@code = response.status
@results = response.body
erb :results
end
...
Views and Adding Styles
All our views are located in the "views" folder. In here, we also have a layout file — layout.erb
— which all views inherit their structure from. It is similar to the layout file in Rails.
# views/layout.erb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Cost of living calc app</title>
<link
rel="stylesheet"
href="css/bulma.min.css"
type="text/css"
rel="stylesheet"
/>
<link rel="stylesheet" href="css/style.css" rel="stylesheet" />
</head>
<body>
<!-- navbar partial -->
<%= erb :'navbar' %>
<!-- //navbar -->
<div><%= yield %></div>
</body>
</html>
We also add a local copy of Bulma CSS and a custom stylesheet in public/css
to provide styling for our app.
Running the Sinatra App
To run a modular Sinatra app, you need to include a config.ru
file where you specify:
- The main file that will be used as the entry point.
- The main module that will run (remember that modular Sinatra apps can have multiple "apps").
# config.ru
require File.join(File.dirname(__FILE__), 'app.rb')
run LivingCostCalc::App
Deploying Your Sinatra App to Production
A step-by-step guide for deploying a Sinatra app to production would definitely make this tutorial too long. But to give you an idea of the options you have, consider:
- Using a PaaS like Heroku.
- Using a cloud service provider like AWS Elastic Cloud or the likes of Digital Ocean and Linode.
If you use Heroku, one thing to note is that you will need to include a Procfile in your app's root:
web: bundle exec rackup config.ru -p $PORT
To deploy to a cloud service like AWS's Elastic Cloud, the easiest method is to Dockerize your app and deploy the container.
Monitoring Your Sinatra App with AppSignal
Another thing that's very important and shouldn't be overlooked is application monitoring.
Once you've successfully deployed your Sinatra app, you can easily use Appsignal's Ruby APM service. AppSignal offers an integration for Rails and Rack-based apps like Sinatra.
When you integrate AppSignal, you'll get incident reports and dashboards for everything going on.
The screenshot below shows our Sinatra app's memory usage dashboard.
Wrapping Up and Next Steps
In this post, we learned what Sinatra is and what you can use the framework for. We then built a modular app using Sinatra.
You can take this to the next level by building user authentication functionality for the app.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Top comments (0)