DEV Community

Felice Forby
Felice Forby

Posted on

The Basics of Sinatra à la MVC — Configuration & Routes & Forms

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

Top comments (3)

Collapse
 
fabien profile image
Thoux • Edited

Hy, thanks for this tuts.

But, donc working for me. I don't know why, i have this warning :

''NoMethodError at /recipes/new
undefined method `find' for Recipe:Class
file: application_controller.rb location: block in class:ApplicationController line: 24''

Regards

Collapse
 
joshuawoodsdev profile image
JoshuaWoods

OMG thank you!!! We need more straight forward tutorials ! This was great

Collapse
 
morinoko profile image
Felice Forby

Yay! I'm glad it helped!