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.
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:
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.