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!