Build a Sinatra MVC Framework

Darren Jones
Share

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.

CSS Master, 3rd Edition