Structuring Sinatra Applications

by Alex MacCaw

I love Sinatra. I use it for practically everything. For me, Sinatra has the perfect blend of simplicity and abstraction. I can understand the full stack, dip into the source when I need to, and extend the framework how I see fit.

However the bare-bones nature of Sinatra can come at a cost. Sinatra leaves much more of the decision making around your application's structure, layout and libraries up to you. This can be a bit daunting if you're just getting into Ruby web app development, which I think is one of the reasons why Sinatra is mainly the realm of more experienced developers looking for a bloat-free alternative to Rails.

Over the years I've put together a few conventions that I share between all my Sinatra apps, and in this article I'm going to elaborate on those. I've also created a gem called Trevi which, if you want to follow this approach, will go ahead and do all the manual lifting for you by generating a Sinatra scaffold.

$ gem install trevi

$ trevi brisk

    create  brisk
    create  brisk/.gitignore
    create  brisk/Gemfile
    create  brisk/Procfile
    create  brisk/Rakefile
    create  brisk/app.rb
    # ...
    run  git init . from "./brisk"
    run  bundle from "./brisk"

Bundler

First we're going to setup Bundler and the gems our application is going to require. Run bundle init to create our Gemfile. It should look something like the following:

source 'https://rubygems.org'
ruby '2.0.0'

gem 'sinatra', require: 'sinatra/base'
gem 'erubis'
gem 'rake'
gem 'thin'

We're requiring sinatra/base, rather than plain sinatra, since we're using the modular Sinatra style, which essentially means we're using modules and namespaces rather than the classical style of putting everything inline. There's nothing wrong with the classical style for simple applications, but if you're planning on building anything complex I'd definitely go with modular.

Run bundle install to generate our Gemfile.lock.

App

Next let's create a namespace for the application, which everything will live under. Don't spend too much time deliberating over the name of the app — just choose an internal code name for the namespace (for example, Brisk). Let's create the application's initial file called app.rb

require 'rubygems'
require 'bundler'

Bundler.require

module Brisk
  class App < Sinatra::Application
    configure do
      disable :method_override
      disable :static

      set :sessions,
          :httponly     => true,
          :secure       => production?,
          :expire_after => 31557600, # 1 year
          :secret       => ENV['SESSION_SECRET']
    end

    use Rack::Deflater
  end
end

In this file we're first requiring all our gems via Bundler, then we're setting up a little Sinatra application. Notice that we're enabling sessions and disabling static files.

Next up we need to create a config.ru file for rack. This will just require our app.rb file, and mount Brisk::App.

require './app'

run Brisk::App

So far so good. Now let's look into adding models and other routes into the mix.

Autoload

Similar to Rails, I like having one main app directory, containing a models, routes and views child dirs. I use autoload to explicitly load in files (rather than using ActiveSupport's Dependencies). It looks like the following:

module Brisk
  module Models
    autoload :Post, 'app/models/post'
  end
end

autoload differs from require in that it lazy loads modules the first time they're referenced, rather than at runtime. Now autoload is not without its caveats which you should understand before using it. It's not thread safe, and may even be deprecated in future version of Ruby. It's for this reason I'm slightly wary about suggesting people use it. However, it personally solves my needs just fine.

To get autoload working correctly, you'll also need to ensure that the current directory is in the app's load path. You can do this by appending the CWD to the $LOAD_PATH (aliased to $:). Add the following to app.rb:

$: << File.expand_path('../', __FILE__)

MVC

So our application has one main route: App. If we want to go about creating another route, we just need to mount it on App. Essentially, each route is its own separate application. For example, we could have a Posts route under app/routes/posts.rb (routes are always plural).

module Brisk
  module Routes
    class Posts < Sinatra::Application
      error Models::NotFound do
        error 404
      end

      get '/posts/:slug' do
        @post = Post.first!(slug: params[:slug])
        erb :post
      end
    end
  end
end

Then we just have to mount Posts in App:

module Brisk
  class App < Sinatra::Application
    # ...

    use Rack::Deflater
    use Routes::Posts
  end
end

If you take away one thing from this article, then this should be it. De-couple your application by splitting it up into lots of smaller applications mounted under one base. Believe me, this is the secret to keeping things simple and clean.

Sharing between routes

If we want to share some functionality between routes, say some configuration, we can use inheritance. Let's define a Base route, which all the other routes inherit from.

module Brisk
  module Routes
    class Base < Sinatra::Application
      configure do
        set :views, 'app/views'
        set :root, App.root

        disable :static

        set :erb, escape_html: true,
                  layout_options: {views: 'app/views/layouts'}
      end

      helpers Helpers
    end
  end
end

In the example above, we're setting up some basic :erb configurations (important for XSS security), and setting the route's CWD. Notice we're also importing a Helpers module which will contain global helpers.

Now we have this Base class, we can inherit from it in other routes to share its functionality. For example, if we wanted to have a authentication layer we could implement it as a Sinatra extension, import it into Base, and all our routes would have access to it.

Models

Database-wise I always choose Postgres. I've found, in the long run, document orientated databases aren't worth the hassle. Postgres is incredibly flexible, rock solid, and has never let me down. It's also pretty convenient because I host everything on Heroku and they have a great Postgres option.

For an ORM I like to use Sequel. In my opinion, it's hands down the best Ruby ORM out there, and strikes the right balance between too much magic and a good abstraction. Crucially the source code is readable and well written, something I check before using any new library.

I also use the project sinatra-sequel, which adds a nice DSL around setting up a database. For migrations, I have a few Rake tasks to help me.

namespace :db do
  desc 'Run DB migrations'
  task :migrate => :app do
   require 'sequel/extensions/migration'

   Sequel::Migrator.apply(Brisk::App.database, 'db/migrations')
  end
end

For more information about Sequel's migrations, see the documentation.

Assets

I use CoffeeScript for JS and Stylus for CSS on the client side, and compile them using Sprockets. Even if you're not using a language that needs to be compiled, I recommend using an asset manager like Sprockets to give you concatenation and cache expiry.

Since Sprockets has great Rack support, integrating Sprockets and Sinatra is fairly straight forward. Just add another route called Assets which will serve up Sprockets compiled assets in development. In production, you can just pre-compile them and serve them statically.

module Brisk
  module Routes
    class Assets < Base
      configure do
        set :assets, assets = Sprockets::Environment.new(settings.root)
        assets.append_path('app/assets/javascripts')
        assets.append_path('app/assets/stylesheets')
      end

      get '/assets/*' do
        env['PATH_INFO'].sub!(%r{^/assets}, '')
        settings.assets.call(env)
      end
    end
  end
end

I highly recommend turning on caching for Sprockets, which will make it much faster to use in development and in deploys. In development use Sprocket's FileStore, and in production I use Memcache. For more information, check out this article I wrote on speeding up deploys to Heroku.

ENV vars

I never hard-code passwords or API keys in the source code, or expose them in the GIT repository. Rather I use environmental variables. In development, I store test keys in a .env file, which is ignored by Git, and picked up automatically by the dotenv gem.

Conclusion

As mentioned previously, if you're interested in building applications in this style, check out the Trevi gem. Also if you want to check out the source code of a real application using this structure, Monocle is a good example.

You may be wondering why, if I've gone to all this trouble to create a scaffold, I don't just use an existing framework like Rails. My main motivation is to keep the stack as small as possible and customized to the task at hand.

At the end of the day, I like very simple abstractions and I like tinkering. I think the main contention point between differences in programmer's opinions is the amount of abstraction they're happy with. Sinatra happens to be at a good level for me.