Lightweight Web Apps: Getting Started With Sinatra

For many reasons, I’m a huge fan of Rails for building web software, and by implication, coding in Ruby. When you’ve spent a little time working with Ruby, it’s difficult to go back to more traditional languages such as PHP.

For less intense website or small applications, though, Rails can feel a bit heavy-handed. Whilst Rails 3 goes a long way to improving this, I’ve recently been trying out the lightweight ruby framework, Sinatra.

Sinatra is a barebones framework for getting web applications up and running quickly. The idea is that Sinatra gives you a base into which you pull together the things you need, effectively building your own mini-framework. The process is enhanced by the vast array of Ruby Gems that are available.

In this post, I’ll show how to build a simple Sinatra application configured with Bundler and running on Rack that lets you quickly start building lightweight web applications.

Creating Your Sinatra Project

Unlike Rails, Sinatra doesn’t come with generators, so you’ll need to create the project layout yourself. Whilst you can use your own folder layout, there are a few conventions for which Sinatra is configured. Something like this should be good to get started:

mkdir -p myproject myproject/views myproject/public myproject/public/javascript myproject/public/css myproject/public/images
touch myproject/app.rb myproject/config.ru myproject/Gemfile

Running these commands generates the following folder structure:

myproject/
  public/
    public/css
    public/images
    public/javascript
  views/
  tmp/
  app.rb
  Gemfile
  config.ru

If you’ve used Rails 3, or worked with the Bundler gem and Rack, most of this should look familar: Gemfile is used by Bundler to declare rubygems the project relies on, whilst config.ru is used by Rack to configure and run the application.

Bundling Gems

Next, edit the Gemfile to load Sinatra and any dependencies. My markup of choice for HAML for layout (HTML) and SASS for CSS stylesheets. Sinatra makes using HAML (or any other renderer, such as ERB) very simple:

# Gemfile
source :rubygems
 
gem "sinatra"
gem "haml"

To install the bundle, just run bundle install from within your project folder:

cd myproject
bundle install

Configure Sinatra to run on Rack

My development environment is setup to run Ruby projects on Rack using the Passenger gem. To configure Sinatra to run on Rack, our project just needs a config.ru file. This file tells Rack to load the any required gems and star your Sinatra application:

# Gemfile
require "rubygems"
require "bundler/setup"
require "sinatra"
require "haml"
require "app"
 
set :run, false
set :raise_errors, true
 
run Sinatra::Application

Build Your App

With the environment configured, it’s time to build a small app to test everything. Start by adding a root path to app.rb and a corresponding index.haml file under views/:

# app.rb
set :haml, :format => :html5
 
get "/" do
  haml :index
end
# views/index.haml
%html
  %head
    %title My Sinatra App
 
  %body
    %h1 Hello World
    %p Your app is up and running!

The get block routes any HTTP GET requests for the site’s root path (example.com/) and renders views/index.haml using the HAML parser.

Running Your Application

To see your application running, start Rack by calling rackup from your project directory.

cd myproject
rackup

Now navigate to localhost:9292 to see your Sinatra app working! All being well, you should be greeted with the following:

Summary

Initially I had trouble seeing how Sinatra would be useful given the existence of full-stack frameworks like Rails. However, as I’ve taken the time to learn Sinatra, I can see the benefits of such a lightweight framework when used for certain tasks.

Whilst it shouldn’t be thought of as a replacement for frameworks like Rails, Sinatra is another tool available to Ruby developers looking to quickly build and deploy small websites and applications.

When Should You Use Sinatra?

When you should use Sinatra over a larger framework like Rails is entirely dependent on the requirements of your site. Sinatra is capable of connecting with databases using gems like ActiveRecord or DataMapper in the same way Rails is. It’s feasible, then, to build large-scale web applications with Sinatra – but my personal preference would be to stick with Rails.

The rule of thumb I’m using at the moment is that if my app requires a database, I’ll build it in Rails, otherwise I’ll try Sinatra.

With that in mind, I’m now using Sinatra to build and run lightweight websites such as the recently launched portfolio of Mark Stocks. Mark’s site only requires simple server-side processing for handling contact forms – a full-blown Rails build seemed a be too much! Sinatra provided just the right base on which to build Mark’s site, whilst retaining the flexibility that comes with the Ruby ecosystem.

Next Steps

In the next tutorial, I’ll extend the basic framework we’ve started here to include of a number of useful Ruby gems built for Sinatra, and show how to use your own custom helper code. When it’s a little more polished, I’ll also publish my working Sinatra base application on github.

References

  1. The Sinatra Website
  2. The Official Sinatra Book
  3. Bare Sinatra App For Deploying To Passenger

Code: Being Conventional

Moving to Ruby on Rails has been great for helping to develop my coding skills, and I’m now adapting what I produce in other languages and projects to take advantage of the patterns used in Rails. One of the most useful has been convention over configuration:

[Convention over configuration] decreases the number of decisions that developers need to make, gaining simplicity, but not necessarily losing flexibility.

Convention over Configuration, Wikipedia

In this sense, being conventional is writing code that meets expectations. Asking what another developer would expect my code to do without their intervention? An example in Rails is the options hash used at the end of many method signatures (e.g. link_to, ActiveRecord::Base.find, etc.). It’s far easier to be able guess a method’s name or signature based on convention, rather than looking up API documentation.

Using convention throughout your project reduces the learning overhead for developers who are new to the team. Documentation becomes easier to write and maintain; it could simply be a reference to another well-maintained document. Even if your team hasn’t written documentation, following convention means that some form of help is likely to be only a web search away. This is much better than a bespoke set of ideas that are buried in the mind of the developer who left sometime last year.

Below are some of the conventions I’ve recently been working to adhere to:

Subversion

Although I’m moving everything to git, I still use Subversion at work. The SVN book recommends that that your trunk folder should always be stable and releasable, whilst development should be carried out in branches, which are then reintegrated to the trunk as they are finished.

Functions and Methods

Many method signatures in Rails follow the format method_name(required_param, [options hash]) where options is a single hash of optional parameters. Every option is given a reasonable default, even if that default is nil). I find this convention great, as often code I’ve written has suffered from ‘parameter bloat’ as requirements change and expand. I’m now implementing the options hash convention not only into Ruby code, but also PHP projects using array_merge, e.g:

public function method_name(id, array $options = array())
{
  $options = array_merge(array(
    'option_1' => 'default',
    'option_2' => 'default'
  ), $options);
  # ...
}

Deployment Scripts

In the past, I’ve written bash scripts to handle checkouts and uploads, but these are always fragile as they are heavily coupled with the underlying development and server environments. Since learning about and using Capistrano for Rails, I am now moving all my projects to use capistrano recipes. I can’t recommend this enough, and have used it for both Rails and PHP projects, with Subversion and git backends. Capistrano is well-document, adaptable, and under active development.

Server Configurations

It makes deployment easier to match your development environment exactly to your testing and live environments; a different point release of PHP or some abstract library can lead to hours of unnecessary debugging. To help keep everything in sync, use your distribution defaults where you can. For example, choose a Linux distribution and use the default packages that it provides (I use Debian 5 stable). It’s also much easier to rebuild or duplicate your server if you can just script an installation to get your server up and running. If you do need specific functionality or newer libraries, compile them into a package (e.g a .deb) and keep them somewhere safe for future use.

Using convention doesn’t mean building dull code. Rather, it helps to encourage debate, and drive innovation in developing better practice. As shown by ideas such as open-source, freely discussing, developing, and refining helps to define conventions and improve the experience for all developers.

Do you use conventions in your code and software projects? Please share and discuss in the comments.

Rails Projects – Call For Advice

I’m about to start working on a large Rails project at work, and hoped to seek advice on working with larger projects using the framework. Most of the tutorials and books I’ve read are written for a single developer working on the code; our project, though, will be worked on by several developers and other staff. One of the decisions I think we need to get right from the start is structuring our development database environment.

On other projects, we have worked with a single development database, so migrations and changes have been applied as required, and everyone’s database is up-to-date. With Rails’ migrations, we have the option of each developer working on their own local database, and merging migrations at commit to the repository. Alternatively, with a single development database, migrations could be applied as required, but would affect all developers regardless of the branch of code they are working on.

Both methods seem to carry their own risks of conflicts and breaking the database schema, so I thought I’d open it up for debate, and would greatly appreciate any advice. What are your tips for setting up and running a team project using Rails?

Agile Development Course

This week I attended a workshop on agile development – Crawl Before You Leap: An Introduction to Agile Development run by AgileLab. The course leaned towards agile project management more so than agile coding practice, and stressed the use of stories and iterations, tests and reflection. As I am normally more focussed on the code-aspects of agile practice, there were some great new concepts and new angles for me – as well as a bunch of handy tips.

As I’m about to start a new Rails project (which advocates agile development), I should get the chance to put some of the theory into practice. Meanwhile, I’m also looking to go to the more coding-biassed course by the same people sometime soon.

Core Data: Cached Transient Properites

In writing HostManager 2.0, I’ve come up against a lot of problems caused by the dark magic that is Cocoa bindings and key-value observing. One of the most troublesome of these has been with transient (calculated) properties.

For example, a Client managed object can get a count of owned domains that are expiring soon. This is a transient propety, accessed through a key of expiringDomains. It simply returns an NSSet which is built from a fetchRequest.

Previously, I was running this fetchRequest each time the key was accessed. This was admittedly inefficient, but seemed functional – at least until you saved the document.

Something crazy happens when a Core Data document is saved – all the objects in a context are released (faulted). There is a good reason for this, as it frees up any memory that was being used to temporarily store the managed objects. However, it caused very strange results to appear in my transient properties; namely, they would all vanish.

Even forcing the fetch request to refresh didn’t pick up any objects from the context (I’m not sure why as I would have expected the faulted objects to be ‘refired’). So I took the plunge and refactored the Client object to cache its expiringDomains value. After some extensive testing and finger-crossing, I’m fairly sure this is the correct way to handle transient relationship properties:

CustomManagedObject.h

@interface CustomManagedObject : NSManagedObject {
ame NSSet *transientRelationship;
}
 
- (NSSet *)transientRelationship;

CustomManagedObject.m

#import "CustomManagedObject.h"
 
@implementation CustomManagedObject
 
- (void)awakeFromFetch
{
ame [super awakeFromFetch];
 
ame // set managed object to observe changes to the context, this is necessary
ame // to monitor changes to the context that occur elsewhere in the app
ame [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh:) name:NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
 
ame // clear the cache after managed object is fetched into the context
ame if( transientRelationship != nil )
ame [transientRelationship release], transientRelationship = nil;}
 
- (void)refresh:(NSNotification *)notification
{
ame // ... perform check to see if update is neccessary, assume it is...
 
ame if( refreshNeeded ) {
ame if( expiringDomains != nil )
ame gDomains release], expiringDomains = nil;
ame }
}
 
- (NSSet *)transientRelationship
{
ame [self willAccessValueForKey:@"transientRelationship"]
ame
ame if( transientRelationship == nil ) {SFetchRequest *aFetchRequest = [[[NSFetchRequest alloc] init] autorelease];
 
ame [aFetchRequest setEntity:[NSEntityDescription entityForName:@"DestinationEntity" inManagedObjectContext:[self managedObjectContext]]];
 
ame // optionally, set a predicate, e.g. owner == self
ame [aFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"predicateString"]];
 
ame NSArray *results = [[self managedObjectContext] executeFetchRequest:aFetchRequest error:nil];
 
ame transientRelationship = [[NSSet setWithArray:results] retain];
ame }
ame
ame [self didAccessValueForKey:@"transientRelationship"]
 
ame return transientRelationship;
}