DEV Community

Cover image for How to Use Sinatra to Build a Ruby Application
Aestimo K. for AppSignal

Posted on • Originally published at blog.appsignal.com

How to Use Sinatra to Build a Ruby Application

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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

  ...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Sinatra dashboard in AppSignal

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)