Build a Sinatra MVC Framework

Share this article

In the last chapter of my book, Jump Start Sinatra, I suggested that the code produced from the book could be refactored into a MVC structure similar to Rails. I left this as an exercise for the reader to complete, but have decided to write up my attempt on here. If you don’t have the book then shame on you(!), but you should be able to follow along with most of what is written here in a general sense. The excellent Padrino shows what can be achieved by building on top of Sinatra, so it should be a fun project to build a microframework from scratch that could be used to make organizing code a bit easier, while maintaining the simplicity of Sinatra.

Songs By Sinatra

In the book, we build a sample application called Songs by Sinatra. This is a site dedicated to the songs of the great Frank Sinatra. It allows a user who is logged in to add songs by Ol’ Blue Eyes, including the title, date, length and lyrics. It also allows visitors to the site to ‘like’ these songs. The live site can be seen here. songsbysinatra

Creating the File Structure

The first job is to create the file structure. Since we are using an MVC structure, it makes sense to have ‘models’, ‘views’ and ‘controllers’ folders. I also decided to create a ‘helpers’ folder and ‘lib’ folder for any extensions and middleware. In the book, we created a small piece of middleware to handle assets (such as CoffeeScript and Sass files), so we have an ‘assets’ folder, as well as the standard public folder for all the publicly available resources, such as images. Here’s a diagram of my folder structure: framework-directory-structure

One Controller to Rule them All

I also suggest in the book to write a global controller called ApplicationController. This should use all the views, layouts and register any extensions used by the whole application.
$:.unshift(File.expand_path('../../lib', __FILE__))

require 'sinatra/base'
require 'slim'
require 'sass'
require 'coffee-script'
require 'v8'
require 'sinatra/auth'
require 'sinatra/contact'
require 'sinatra/flash'
require 'asset-handler'

class ApplicationController < Sinatra::Base

  helpers ApplicationHelpers

  set :views, File.expand_path('../../views', __FILE__)
  enable :sessions, :method_override

  register Sinatra::Auth
  register Sinatra::Contact
  register Sinatra::Flash

  use AssetHandler

  not_found{ slim :not_found }
end
This requires all the necessary gems that are used and then creates an ApplcationController class. This will be the base class for all controllers. It registers the ApplicationHelpers, which will be where all application-wide helpers go. The views folder needs to be set here, as it is not located in the same directory as our application_controller.rb file which is what Sinatra expects. This is easy to change, though, using the set command. We also enable sessions and method_override here. Sessions are needed to use sinatra-flash and will also be required for most applications. The method_override setting is used to allow browsers to support HTTP methods such as PUT, PATCH and DELETE using a POST method and hidden input field in a form.

Extensions

In the book, I went through building an Auth
extension module. I also explained how to write some helper methods for sending a contact email. I extracted these and the contact routes into their own extension so that it could be set separately The settings for these can be set in the ApplicationController, so the code in the extensions doesn’t need to be edited at all.

Other Controllers

The other controllers now inherit from the ApplicationController class. There are two controllers in this application – the WebsiteController, responsible for the main part of the site and the SongController, responsible for all the CRUD operations performed on the Song model.
class WebsiteController < ApplicationController
  helpers WebsiteHelpers

  get '/' do
    slim :home
  end

  get '/about' do
    @title = "All About This Website"
    slim :about
  end
end

class SongController < ApplicationController
  helpers SongHelpers

  get '/' do
    find_songs
    slim :songs
  end

  get '/new' do
    protected!
    find_song
    slim :new_song
  end

  get '/:id' do
    find_song
    slim :show_song
  end

  post '/songs' do
    protected!
    create_song
    flash[:notice] = "Song successfully added"
    redirect to("/#{@song.id}")
  end

  get '/:id/edit' do
    protected!
    find_song
    slim :edit_song
  end

  put '/:id' do
    protected!
    update_song
    flash[:notice] = "Song successfully updated"
    redirect to("/#{@song.id}")
  end

  delete '/:id' do
    protected!
    find_song.destroy
    flash[:notice] = "Song deleted"
    redirect to('/')
  end

  post '/:id/like' do
    find_song
    @song.likes = @song.likes.next
    @song.save
    redirect to("/#{@song.id}") unless request.xhr?
    slim :like, :layout => false
  end
end

Models

There is only one model in this case – the Song model. In the model directory, there is just one file song.rb that creates the Song class and sets up all the DataMapper properties:
require 'dm-core'
require 'dm-migrations'

class Song
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :lyrics, Text
  property :length, Integer
  property :released_on, Date
  property :likes, Integer, :default => 0

  def released_on=date
    super Date.strptime(date, '%m/%d/%Y')
  end

  DataMapper.finalize
end
Note that I’m using DataMapper for this model, but could easily use another ORM. In fact, I could even use a different ORM in a another model.

Helpers

Each controller has its own helper file, so there is application-helpers.rb, website-helpers.rb, and song-helpers.rb. These are created as a module and then each controller has to explicilty register the associated helpers module. Here is the application helpers file:
module ApplicationHelpers
  def css(*stylesheets)
      stylesheets.map do |stylesheet|
        "<link href="/#{stylesheet}.css" media="screen, projection" rel="stylesheet" />"
    end.join
  end

 def current?(path='/')
   request.path_info==path ? "current": nil
 end
end
It is registered in application_controller.rb with the following line:
helpers ApplicationHelpers
The application helpers are where all the global helper methods go. The two above are used in the layout to make it easier to include links to CSS files and another helper to add a class of ‘current’ to a link if it is linking to the current page.

Rackup

The config.ru is where all the configuration is done:
require 'sinatra/base'

Dir.glob('./{models,helpers,controllers}/*.rb').each { |file| require file }

SongController.configure :development do
  DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/development.db")
end

SongController.configure :production do
  DataMapper.setup(:default, ENV['DATABASE_URL'])
end

map('/songs') { run SongController }
map('/') { run WebsiteController }
First of all we require Sinatra::Base (as opposed to Sinatra, since we are using a modular-style structure). Then we require all the files kept in the models, helpers and controllers folders. After this we do a bit of configuration for the SongController class to set up the database for the production and development environments. This could have been placed in the song_controller.rb file, but the config.ru seemed the best place to put the configuration. Last of all we use the handy map methods that Rack gives us to create a namespace for each controller.

That’s All Folks

Sinatra’s flexibiltiy and modular style made it very easy to refactor the application into a framework-style structure. I think it hits a nice balance between code organization and still keeping things relatively simple. You still have to do a few more things by hand – like setting up views and database connections by hand. Going forward I think I’d like to look at perhaps adding a few more extensions, using a better Asset Handler and organizing the database and ORM a bit better. I’ve put all the code on GitHub and called it ‘Jump Start’. The name seems doubly appropriate given how it came about and because it helps you ‘jump start’ your Sinatra projects. Would you use Jump Start? Can you think of any ways to improve it? Have you created your own bespoke framework using Sinatra? As usual, let us know in the comments below.

Frequently Asked Questions (FAQs) about Building a Sinatra MVC Framework

What is Sinatra MVC and why is it important?

Sinatra MVC (Model-View-Controller) is a design pattern used in software development. It separates an application into three interconnected parts: the Model, the View, and the Controller. This separation allows developers to manage complex applications and systems by breaking them down into smaller, more manageable pieces. It’s important because it promotes organized programming and enhances the scalability and maintainability of applications.

How does Sinatra MVC differ from other MVC frameworks?

Sinatra MVC is a lightweight and flexible framework. Unlike other MVC frameworks like Rails, Sinatra doesn’t come with many built-in tools or functions. This means developers have more freedom to customize their applications, but it also means they may need to write more code to achieve the same results.

How do I set up a Sinatra MVC framework?

Setting up a Sinatra MVC framework involves several steps. First, you need to install the Sinatra gem. Then, you need to create a file structure for your application, which includes separate directories for your models, views, and controllers. After that, you can start building your application by defining routes in your controller, creating views to display data, and setting up models to interact with your database.

What is a controller in Sinatra MVC?

In Sinatra MVC, a controller is a component that handles the logic of your application. It receives requests from the user, interacts with the model to process these requests, and then sends the appropriate response back to the user. The controller is essentially the intermediary between the model and the view.

How do I use ‘include’ in my Sinatra application controller methods?

The ‘include’ keyword in Ruby is used to mix in modules into classes. This allows you to share methods between different classes. In a Sinatra application, you might use ‘include’ to add helper methods to your controller. These methods can then be used in any of your controller’s actions to perform common tasks.

How do I configure routes in Sinatra MVC?

Routes in Sinatra MVC are defined in the controller. Each route corresponds to a URL pattern and an HTTP method (like GET or POST). When a user sends a request that matches a route, Sinatra will execute the corresponding block of code. This block can interact with the model, render a view, or send a response directly to the user.

How do I create forms in Sinatra MVC?

Forms in Sinatra MVC are created in the view. They allow users to input data that can be sent to the server. When a user submits a form, the data is packaged into a request and sent to a route defined in the controller. The controller can then process this data and interact with the model as necessary.

How do I interact with a database in Sinatra MVC?

Interacting with a database in Sinatra MVC is done through the model. The model is responsible for creating, reading, updating, and deleting records in the database. This is often done using an Object-Relational Mapping (ORM) tool like ActiveRecord, which allows you to interact with your database using Ruby code.

How do I test my Sinatra MVC application?

Testing a Sinatra MVC application can be done using a testing framework like RSpec. This allows you to write tests for your models, views, and controllers to ensure they are working as expected. Testing is an important part of the development process, as it helps you catch and fix bugs before they become a problem.

How do I deploy my Sinatra MVC application?

Deploying a Sinatra MVC application involves transferring your code to a server where it can be accessed by users. This can be done using a platform like Heroku, which provides a simple way to deploy and manage web applications. Once your application is deployed, you can monitor its performance, fix bugs, and make updates as necessary.

Darren JonesDarren Jones
View Author

Darren loves building web apps and coding in JavaScript, Haskell and Ruby. He is the author of Learn to Code using JavaScript, JavaScript: Novice to Ninja and Jump Start Sinatra.He is also the creator of Nanny State, a tiny alternative to React. He can be found on Twitter @daz4126.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week