Felice Forby
Posted on July 24, 2018
Sinatra is a DSL that lets you easily get your application up and running on its own web server which can respond to HTTP requests and handle URI routing. It is actually built on Rack, a webserver interface for Ruby apps. Compared to Rack, Sinatra is much easier to use.
This blog post goes over basic set for Sinatra using the MVC model and getting started with basic routes using the example of a simple recipe app.
File Structure and Initial Setup/Configuration
The following is one of the basic ways to organize an app in a Model-View-Controller setup. Notably, all the main app files are organized inside an app folder with subfolders corresponding to each component of MVC: /models
for your object model files, /views
for your html template files, and /controllers
for your controller files.
CSS stylesheets and Javascript often go into the public folder under their own subfolders. The spec folder is for spec tests, if you have them.
├── Gemfile
├── README.md
├── app
│ ├── controllers
│ │ └── application_controller.rb
│ ├── models
│ │ └── model.rb
│ └── views
│ └── index.erb
├── config
│ └── environment.rb
├── config.ru
├── public
│ └── stylesheets
│ └── javascript
└── spec
├── controllers
├── features
├── models
└── spec_helper.rb
*Note that for smaller applications, sometimes a single application controller is all that is need and may be put directly in the root folder. The models and views folders may also be put directly in the root instead of an app folder.
Gemfile
First, you’ll need the Sinatra gem and I’d recommend the Shotgun gem as well, so you don’t need to restart the Sinatra server during testing/development every single time you make a change. Make sure your Gemfile includes the following:
# Gemfile
source "https://rubygems.org"
gem 'sinatra'
gem 'shotgun'
After the Gemfile is ready, make sure you run bundle install
in the command line.
Config.ru
Sinatra and Rack-based apps need a config.ru
that loads the environment and other requirements of your app, specifies which controllers should be used via the use
and run
keywords, and starts the application server when run
is called.
In the simple case below, the application_controller.rb
file is our only controller and environment is loaded via the config/environment.rb
file.
# config.ru
require_relative './config/environment'
run ApplicationController
config/environment.rb
This file gets our application code connected to the appropriate gems. It loads Bundler (and thus all the gems in the Gemfile) and all the files (models, views, and controllers) in the app directory.
ENV['SINATRA_ENV'] ||= "development"
ENV['RACK_ENV'] ||= "development"
require 'bundler/setup'
Bundler.require(:default, ENV['SINATRA_ENV'])
require_all 'app'
(To use the handy require_all keyword, add gem 'require_all'
to the Gemfile. Check it out on Github here.)
Application Controller
Okay, let’s finally look at a basic controller for Sinatra. The controller’s job is to handle all the incoming requests, responses, and routing.
In the application_controller.rb
file, let’s create a class for the application that inherits from Sinatra::Base
. Sinatra::Base
gives our app a Rack-compatible interface that can be used via Sinatra’s framework.
# application_controller.rb
class ApplicationController < Sinatra::Base
# code for the controller here...
end
To get the controller set up for the file structure above, add some configuration code that tells Sinatra where to find the /views
folder (Sinatra looks for it in the root by default) and the /public
folder using a configure
block. After the configuration code, we can write our routes (I’ll go over some basics below).
# application_controller.rb
class ApplicationController < Sinatra::Base
configure do
set :views, "app/views"
set :public_dir, "public"
end
get '/' do
"Hello World!"
end
# and more routes...
end
For a small application, a single controller will be enough and here we can define all the URLs and how they will respond to requests like ‘get’ and ‘post.’
Note that the application controller class can be named anything you like; just make sure it’s mounted with the appropriate name via run
in the config.ru
file. “Mounting” is just telling Rack which part of your application is defining the controller that handles web requests.
Routes
Basic routes
Routes are what connect requests from a browser to the specific method in your app (in the Controller) that can handle dealing with the request and sending a response. For example, on the simple side, a route might just show, or render, a basic HTML view. Or, another route might receive data submitted via a form, say a recipe title and its ingredients and steps, process that data, and then show the completed post–a new recipe, which is just another HTML view.
Some basic GET request could look like this:
# application_controller.rb
get '/' do
erb :index
end
get '/about' do
erb :about
end
The get '/' do
and get '/about' do
lines correspond to the URLs in the browser. So, if the domain is tastybites.com
, get '/' do
refers to that root domain. get '/about' do
would refer to tastybites.com/about
.
The erb :index
and erb :about
lines tell the controller which view file, in this case an embedded ruby file, in the views folder to get and show. So we would need to have a index.erb
and an about.erb
in a views folder for this to work.
As you can see, the view file is represented by a symbol of the same name in the route. Sinatra assumes that the view templates are all directly under the /views
folder, so if the view happens to be nested in a folder in /views
, say /views/recipes/index
, we will need to refer to it as so: erb :'recipes/index'
or erb 'recipes/index'.to_sym
. For example:
# application_controller.rb
get '/recipes/index' do
erb :'recipes/show'
end
# Or the other way:
get '/recipes/index' do
erb 'recipes/show'.to_sym
end
Dynamic Routes
Dynamic routes can handle a HTTP request based on attributes in the URL. These attributes are represented by represented by symbols coded directly in the route and their values are easily accessible through the automatically generated params
hash, so they can be used to look up or process data.
Let’s look at an example. Let’s say that in our recipe site at tastybites.com
you want to be able to get individual recipes via their id in the URL (e.g. tastybites.com/recipes/27
, with 27 representing the id). Obviously, we wouldn’t want to write out a route for every single recipe and its id. A symbol is used instead: get '/recipes/:id'
do, where the :id
could be any number. The value of :id
is then accessible through params[:id]
. Let see how this could be used to grab the proper recipe:
# application_controller.rb
get '/recipes/:id' do
# The :id is passed through the URL,
# which is accessible in the params hash.
# Use it to assign a recipe to an instance variable
@recipe = Recipe.find(params[:id])
erb :'recipes/show'
end
Notice how the params hash was used to look up a recipe from the database. That recipe was then assigned to an instance variable. Instance variables in Sinatra are super special because we can used them to pass data to our views! Which brings to me the next section…
Passing data to view templates through instance variables
Whenever you create an instance variable with in a controller route, that variable is available within the corresponding view file. Note that the instance variables will not be available within other routes in the controller; only the view specified within a single route.
Let’s go back to the example above:
# controllers/application_controller.rb
get '/recipes/:id' do
@recipe = Recipe.find(params[:id])
erb :'recipes/show'
end
This assumes you also have a recipe model with in your app/models
folder, that could look something like this:
# models/recipe.rb
class Recipe
attr_accessor :title, :description, :ingredients, :method
# The rest of your Recipe class code...
end
The recipe has a few attributes like a title, descriptions, etc., so once the recipe object has been assigned to @recipe
in the controller route, we can weave all those attributes right into our ERB template:
# views/recipes/show.erb
<h1><%= @recipe.title %></h1>
<p><%= @recipe.description %></p>
<h2>Ingredients</h2>
<ul>
<% @recipe.ingredients.each do |ingredient| %>
<li><%= ingredient %></li>
<% end %>
</ul>
<h2>How to Cook</h2>
<p><%= @recipe.method %></p>
We can even iterate through data in views. For example, let’s say in we want to show all the recipes in the recipe index page. First, we might assign all the recipes into an instance variable in the recipes/index
route:
# controllers/application_controller.rb
get '/recipes/' do
@recipes = Recipe.all
erb :'recipes/index'
end
Then, iterate over the recipes in the recipes/index.erb
template:
# views/recipes/index.erb
<h1>All Recipes</h1>
<% @recipes.each.do |recipe| %>
<h2>
<a href="../recipes/<%= recipe.id %>"><%= recipe.title %></a>
</h2>
<% end %>
Notice how we also linked to the recipe page using the recipe id, which will be processed by the get '/recipes/:id'
route. Pretty cool.
Note that these instance variables do not need to be objects from models; they can be any variable that you want to assign and use in the view.
Now that we know how to get and use data from the URL, let’s take a look at processing data that gets sent through forms via the POST method.
Passing data with forms and catching it with ‘post’ routes
Receiving user-input data from forms is the key to building web apps. We just need to correctly hook up your forms to our controllers. Let’s keep going with our recipe example–this time we’re going to create a recipe, so the first thing we’ll need is a basic form and route that will render it.
Setting up the route is as simple as connecting the URL for a new recipe post to the proper view, which is going to contain our form. In this case, let’s say we want the url to be tastybitees.com/recipes/new
:
# app/controllers/application_controller.rb
get '/recipes/new' do
erb :'recipes/new'
end
Next, inside the views/recipes/new.erb
file, we’ll set up a basic form:
# views/recipes/new.erb
<form method='POST' action='/recipes'>
<label for="title">Title</label>
<input type="text" name="title">
<label for="description">Description</label>
<textarea name="description"></textarea>
<label for="ingredients">Ingredients</label>
<textarea name="ingredients"></textarea>
<label for="method">How to Cook</label>
<textarea name="method"></textarea>
<input type="submit" value="Submit">
</form>
The <form method='POST' action='/recipes'>
line is very important for setting up the route. The action
attribute tells the controller what part of the code (that is, which route) should handle the form. Think of it like an address. The method
attribute is simply how it’s going to get there, in this case via POST
.
The other important part is the name
attribute on each input tag, as this is what sets up our params
hash. Above we have name="title"
, name="description"
, name="ingredients"
, and name="method"
, which correspond to the attributes in our Recipe model. This will produce a hash that will look something like this, depending on what the user submitted:
{
"title" => "Apple Pie",
"description" => "A recipe that I learned from my granny.",
"ingredients" => "4 apples, 1 cup sugar, ...",
"method" => "Preheat oven to 350 degrees. Cut the apples in small pieces..."
}
When the form is submitted, this data is now available in this handy-dandy params
hash! So let’s set up the post route and use the params to make a new recipe:
# app/controllers/application_controller.rb
# This is responsible for PROCESSING a newly submitted recipe form
post '/recipes' do
@recipe = Recipe.new
# get data from params
@recipe.title = params[:title]
@recipe.descriptions = params[:descriptions]
@recipe.ingredients = params[:ingredients]
@recipe.method = params[:method]
@recipe.save
end
The only problem with this is that once the form has been submitted, the user is going to be shown a blank page. Here, POST
just sends the information, it doesn’t display it afterwards. After the recipe data has been processed and completed, it makes sense to show the finished recipe to the user. Conveniently, Sinatra has a nice redirect
method that will take the user to another page, in this case the recipe show page. Add it to the above route to get this:
# app/controllers/application_controller.rb
post '/recipes' do
@recipe = Recipe.new
@recipe.title = params[:title]
@recipe.descriptions = params[:descriptions]
@recipe.ingredients = params[:ingredients]
@recipe.method = params[:method]
@recipe.save
# show post after completion
redirect "/recipes/#{@recipe.id}"
end
The redirect "/recipes/#{@recipe.id}"
is going to send it to our recipe show route, get '/recipes/:id'
, so the user can be proud of their new creation.
And there we have, the very basics of setting up a simple Sinatra-based app using MVC concepts! There are a million things you can do with routes and a good place to get more info is right on Sinatra’s README page.
Posted on July 24, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.