Welcome to End Point’s blog

Ongoing observations by End Point people

Ecommerce on Sinatra: In a Jiffy

Several of us at End Point have been involved in a non-ecommerce project for one of our clients running on Ruby, Sinatra, Unicorn, using DataMapper, PostgreSQL, PostGIS, with heavy use of JavaScript (specifically YUI). Sinatra is a lightweight Ruby web framework – it's not in direct competition with Rails but it might be a better "tool" for lightweight applications. It's been a fun project to work with Sinatra, DataMapper, and YUI as I've been working traditionally focused on their respective related technologies (Rails, ActiveRecord, jQuery).

Out of curiosity, I wanted to see what it might take to implement a bare-bones ecommerce store using Sinatra. Here is a mini-tutorial to develop an ecommerce store using Sinatra.

A snapshot of our final working app.

Getting Started

I create a new directory for the project with the following directories:


Data Model

Now, let's look at the data model. Since this is a bare-bones store, I have one order model which contains all the order information including contact information and addresses. We're not storing the credit card in the database. Also, since this is a bare-bones app, we're going to go with one product with a set price and force the limitation that users only buy one at a time. I've also chosen to use ActiveRecord here since I'm still not sold on DataMapper, but another ORM can be used as well. Here is our model:

# sinatrashop/models/order.rb
class Order < ActiveRecord::Base
  validates_presence_of :email
  validates_presence_of :bill_firstname
  validates_presence_of :bill_lastname
  validates_presence_of :bill_address1
  validates_presence_of :bill_city
  validates_presence_of :bill_state
  validates_presence_of :bill_zipcode
  validates_presence_of :ship_firstname
  validates_presence_of :ship_lastname
  validates_presence_of :ship_address1
  validates_presence_of :ship_city
  validates_presence_of :ship_state
  validates_presence_of :ship_zipcode
  validates_presence_of :phone
  validates_format_of :email,
    :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
    :on => :create

And here is our migration:

# sinatrashop/db/migrate/001_create_orders.rb
class CreateOrders < ActiveRecord::Migration
  def self.up
    create_table :orders do |t|
      t.string   :email, :null => false
      t.string   :bill_firstname, :null => false
      t.string   :bill_lastname, :null => false
      t.string   :bill_address1, :null => false
      t.string   :bill_address2
      t.string   :bill_city, :null => false
      t.integer  :bill_state, :null => false
      t.string   :bill_zipcode, :null => false
      t.string   :ship_firstname, :null => false
      t.string   :ship_lastname, :null => false
      t.string   :ship_address1, :null => false
      t.string   :ship_address2
      t.string   :ship_city, :null => false
      t.integer  :ship_state, :null => false
      t.string   :ship_zipcode, :null => false
      t.string   :phone, :null => false

  def self.down
    drop_table :orders

I did some research here and created the Rakefile shown below to run the migrations. The Rakefile establishes a connection to a sqlite3 database and runs migrations in the db/migrate directory.

# sinatrashop/Rakefile
namespace :db do
  task :environment do
    require 'rubygems'
    require 'logger'
    require 'active_record'
    ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
      :database => 'db/development.sqlite3.db'

  desc "Migrate the database"
  task(:migrate => :environment) do
    ActiveRecord::Base.logger =
    ActiveRecord::Migration.verbose = true


Now, let's think about the views we'll present to users. There are many template rendering options in Sinatra, but we'll go with erb and create an index.erb file. By default, Sinatra looks for views in the ROOT/views directory. This will be our only view and layout and below is a breakdown of what it will include:

# header information
# product information
# form for submission
# errors or success message

Obviously, there will be a lot more code here, but the view needs to show the basic product information, the form fields to collection information, and errors or a success message to handle the different use cases. See the code here to examine the contents.

Application Code

Next, let's take a look at the application code. This will be in sinatrashop/store.rb:

require 'sinatra'
require 'erb'
require 'active_record'
require 'configuration'
require 'models/order'

get '/' do
  erb :index

post '/' do
  erb :index

The application code handles two requests, a get and post to '/'. The get is a standard home page request. The post to '/' is the order submission. The post '/' action needs to save the order, establish a connection to the payment gateway, and authorize and capture the payment. If any of these actions fail, the order must not be saved to the database and errors must be presented to the user. Consider the following code, which uses ActiveRecord::Base.transaction method and will rollback the saved order if any part of the authorization fails. We also use ActiveMerchant here, which is an extraction from Shopify for payment gateway integration that can be used as a gem.

# sinatrashop/store.rb
post '/' do
    order =[:order])
    ActiveRecord::Base.transaction do
        params[:credit_card][:first_name] = params[:order][:bill_firstname]
        params[:credit_card][:last_name] = params[:order][:bill_lastname]
        credit_card =[:credit_card])
        if credit_card.valid?
           gateway =

           # Authorize for $10 dollars (1000 cents) 
           response = gateway.authorize(1000, credit_card)
           if response.success?
             order.update_attribute(:status, "complete")
             gateway.capture(1000, response.authorization)
             @success = true
             raise Exception, response.message
           raise Exception, "Your credit card was not valid."
         raise Exception, 'Errors: ' + order.errors.full_messages.join(', ')
  rescue Exception => e
    @message = e.message 


You might notice above that there is a "settings" hash used in the payment gateway connection request. I create a configuration file which sets up some configuration variables in Sinatra's configure do block:

# sinatrashop/configuration.rb
require 'active_merchant'

configure do
  set :authorize_credentials => {
    :login => "LOGIN"
    :password => "PASSWORD"
    :adapter => 'sqlite3',
    :database =>  'db/development.sqlite3.db'
  ActiveMerchant::Billing::Base.mode = :test


I wrote several tests to handle a few use cases. These can be examined here. The tests use Rack::Test and can be run with the command ruby -rubygems test_store.rb.

Infrastructure Concerns

Additional changes are required for running the application on the HTTP server of your choice. Additionally, the entire site should probably run in SSL, which would need configuration. Finally, sqlite may be replaced with a different database here with updates to the configuration.rb and Rakefile files. During development, I ran my app with the command ruby -rubygems store.rb.


Our bare-bones ecommerce app contains a single simple order model that contains all of our order information. There are only two actions defined our application code. There is one index view. Public assets are served from ROOT/public/. The Rakefile contains functionality for running migrations. There is no admin interface here. A site administrator needs to retrieve orders from the database for sending email notifications and fulfillment. In incremental development, this is the simplest setup to allow someone to collect money, but it requires quite a bit of manual management (emails, fulfillment, etc.). The code can be found here. Here are more screenshots of the working application:

Order errors!

Payment gateway errors.

A successful transaction.


Kyle Smith said...

Just a heads up that, for Ruby noobies, there's a typo in the file name for the migration (serious headache if you don't know what you're looking for).

it should be db/migrate/001_create_orders.rb to match the pluralized class name "CreateOrders".

Steph Skardal said...

Thanks for catching that, Kyle!