Paul Sobocinski Paul has over ten years of full-stack web applications development experience. He has built and scaled apps on LAMP stacks as well as on Ruby on Rails. He currently works at Nulogy as a Software Developer, where he enjoys practicing TDD and pair programming on a daily basis.

1 Introduction

In this article, we're going to compare the relative merits of Ruby on Rails and Sinatra by building the same web app in both frameworks.

1.2 Prerequisites

This tutorial assumes you have the following installed and available:

Ruby (install via RVM is recommended) RubyGems Rails 4 ( gem install rails , or gem update rails if you have an older version installed) Bundler ( gem install bundler ) Git (you can also brew install git if you're on a Mac) Heroku Toolbelt PostgreSQL DB (if you're on OS X 10.9, Postgres.app is recommended)

2 Deploying a 'Hello World' app to Heroku

To avoid repeating ourselves, we'll name our apps public-bookmarks-sinatra and public-bookmarks-rails , even though initially they'll do nothing more than print 'Hello World' on a web page.

2.1 Sinatra

First, we create the directory and the Ruby code needed to serve up our "Hello World!" endpoint.

$ mkdir pubic-bookmarks-sinatra $ cd public-bookmarks-sinatra $ cat > public-bookmarks.rb require 'sinatra' get '/' do "#{['Hello', 'Hi', 'Hey', 'Yo'][rand(4)]} World!" end [Ctrl-D]

As you can see, the expressiveness and simplicity of Sinatra allows us to implement our endpoint with four lines of code. Furthermore, if we weren't deploying to Heroku, we could run our app at this point simply by executing:

$ gem install sinatra $ ruby public-bookmarks.rb

However, Heroku needs Gemfile and config.ru files to deploy a Sinatra (or any Rack-based) app. Both of these are simple enough to add:

$ cat > Gemfile source 'https://rubygems.org' gem 'sinatra' ruby '2.1.2' [Ctrl-D] $ cat > config.ru require './public-bookmarks' run Sinatra::Application [Ctrl-D] At this point, you can run the following locally by executing: <!--code lang=bash linenums=true--> $ bundle install $ rackup -p 4567

This will make your app available on http://localhost:4567. To get it up on Heroku, we need to:

Create a Git repo. Create a Heroku app. Push our code to Heroku.

Steps 1 and 2 are accomplished by running the following commands (we're assuming that you have the Heroku Toolbelt installed on your machine):

$ git init $ heroku apps:create

Finally, we make a commit and push the code to Heroku:

$ git add . $ git commit -m 'first commit' $ git push heroku master

To launch the app in your browser, simply run heroku apps:open in your command line, or open the URL that Heroku returns in the deploy message.

2.1.1 Summary

As you can see, building a 'Hello World' app in Sinatra and deploying to Heroku is ridiculously quick and simple; we managed to do it in:

Nine lines of code; Three files; and Six commands.

Now let's compare the process to deploying a 'Hello World' Rails app on Heroku.

2.2 Rails

Rails makes it easy enough to create a Rails app by running a single command:

$ cd .. $ rails new public-bookmarks-rails --skip-test-unit --database=postgresql

The downside (or upside) is that this gives us a lot of boilerplate code, much of it we won't need in this tutorial. This is one of the key differences between Rails and Sinatra. You can exclude some of the boilerplate code by invoking the options available (run rails new --help to view) -- we have done so for JavaScript and Test::Unit.

Next, we create a controller to handle the 'Hello World' request. We do so by using rails generate (more later on this useful tool):

$ cd public-bookmarks-rails $ rails generate controller hello_world index --no-helper --no-assets

As with rails new , this tool is a double-edged sword in that it generates boilerplate code that we might not need. We can view the controller that's generated by running cat app/controllers/hello_world_controller.rb . This outputs:

# app/controllers/hello_world_controller.rb class HelloWorldController < ApplicationController def index end end

Similarly to Sinatra, Rails keeps the code minimal. Invoking an empty controller method (in our case, index ) will render the view found in the directory structure that corresponds to that of the controller. So for us, that's app/views/hello_world/index.html.erb . The rails generate command has already created the view file for us. Let's override it with our version:

$ cat > app/views/hello_world/index.html.erb <%= ['Hello', 'Hi', 'Hey', 'Yo'][rand(4)] %> World! [Ctrl-D]

Note: we could've done this directly in the controller by adding a line to the index method in HelloWorldController , as follows:

# app/controllers/hello_world_controller.rb class HelloWorldController < ApplicationController def index render inline: "<span>#{['Hello', 'Hi', 'Hey', 'Yo'][rand(4)]} World!</span>" end end

However this is not considered good practice in a Rails app as you're working against the framework. On the other hand, inline is the most natural approach with Sinatra. Another not-so-subtle difference in the two frameworks.

Lastly, we need to wire up the application's root route to the controller method we created. If we have a look at the existing config/routes.rb file, we'll see that running rails generate has added a route for us (omitting comments):

# config/routes.rb Rails.application.routes.draw do get 'hello_world/index' end

However this isn't the route we want; we want to route the application's "root" route to our "Hello World" controller action (controller methods that implement endpoints are known as "actions"). We could have left the route in and just had both routes mapped to the same action. However to keep things simple we override the routes.rb file to add the root route on its own:

$ cat > config/routes.rb Rails.application.routes.draw do root 'hello_world#index' end [Ctrl-D]

At this point, we should have everything we need to run our Rails 'Hello World' app. However, there's a small gotcha to overcome. As we've specified PostgreSQL as the DB, we need to ensure that our Rails app has the credentials to connect to it. To do so, we override the existing config/database.yml file as follows:

$ cat > config/database.yml development: adapter: postgresql database: public-bookmarks-rails_development host: localhost username: postgres password: root [Ctrl-D]

Note that the last two configuration options (username and password) may be dependent on your local DB configuration (if you omit username, it will attempt to use the current logged-in user). Also, configuration for test and production environments has been omitted here, as we will not be using a test environment, and Heroku (our production environment) overrides this file completely on deploy.

Now that we have our DB wired up, we have to invoke the rake script to actually create the schema:

$ rake db:create

Note that we could have avoided the last two steps by using the --skip-active-record option when running rails new , but the persistence layer will be useful later, so it makes sense to take advantage of its inclusion. Besides, there aren't many practical use cases for using Rails without ActiveRecord.

At this point you can run the app locally (on http://localhost:3000) by executing rails server in your console.

The last Rails-specific configuration steps prior to deploying to Heroku are to:

Add the '12 Factor' gem (more about that in this Heroku article on Ruby Support) Specify the version of Ruby our app is running on (so Heroku doesn't have to guess).

We accomplish these respectively by appending two lines to the Gemfile, as follows:

$ echo "gem 'rails_12factor', group: :production" >> Gemfile $ echo "ruby '2.1.2'" >> Gemfile

Finally, we can set up the repo and push it to a newly-created Heroku app (this process is identical to what we did for our Sinatra app):

$ git init $ heroku apps:create $ git add . $ git commit -m 'first commit' $ git push heroku master

2.2.1 Summary

Here's what was involved to get our Rails app on Heroku:

12 lines of code added (122 including boilerplate); 4 files added or modified (37 files including boilerplate); and Nine commands executed.

As mentioned before, a key difference between Rails and Sinatra is the inclusion of boilerplate code. The latter provided us with literally zero lines of code off the bat. Consequently, many argue that Sinatra shouldn't be called a "framework"; in fact it defines itself as a DSL for quickly creating web applications in Ruby with minimal effort.

While there were more steps involved in building the 'Hello World' Rails app vs. the Sinatra one, we intend to use some of the boilerplate code that's included. For example, Rails comes with a view layout file ( app/views/layout/application.html.erb ) which is used by default. By comparison, our Sinatra app is just rendering plain text (and thus technically not rendering a valid HTML document). Also, our Sinatra app is missing a persistence layer, which we'll need for Part 2. Let's address these shortcomings next.

3 Extending Sinatra

3.1 Adding layouts

This is simple enough to do with Sinatra out-of-the-box; it involves the following two steps:

Invoke the view in our main app file ( public-bookmarks.rb ). Create the view and layout files.

The first step is simple enough; edit public-bookmarks.rb to look like this:

# public-bookmarks.rb require 'sinatra' get '/' do erb :hello_world end

The next step is to create the view which will render our "Hello World" message:

$ mkdir views $ cat > views/hello_world.erb <%= ['Hello', 'Hi', 'Hey', 'Yo'][rand(4)] %> World! [Ctrl-D]

In our case, we didn't explicitly specify a layout (in public-bookmarks.rb ). This means that Sinatra will first look for a views/layout.erb file. It'll use that if it's found; otherwise, it will render without a layout (essentially the same as rendering inline). As we want it to use a layout, we add that file next:

$ cat > views/layout.erb <!DOCTYPE html> <html> <head><title>PublicBookmarksSinatra</title></head> <body> <%= yield %> </body> </html> [Ctrl-D]

Now when we start our Sinatra app and inspect the source, we should see a valid HTML document.

3.2 Adding persistence (ActiveRecord)

Add the following three lines to your Gemfile:

# Gemfile gem 'sinatra-activerecord' gem 'pg' gem 'rake'

After running bundle install to install the gems, create a config/database.yml file; this has the same content as the DB config file created for the Rails app (except for the DB name):

$ cat > config/database.yml development: adapter: postgresql database: public-bookmarks-sinatra_development host: localhost username: postgres password: root [Ctrl-D]

As in the Rails case, we don't need to specify connection parameters for the production environment. This is due to the functionality of the sinatra-activerecord gem -- when invoked on production, it attempts to read the DATABASE_URL environment variable and use this to connect. This variable is set for you on Heroku when you add the PostgreSQL add-on (this is covered shortly).

A key component of ActiveRecord is the ability to run rake tasks (for schema migrations, seeding the DB, etc.). In order to allow for this, we need to create a Rakefile and "require" the necessary code:

$ cat > Rakefile require 'sinatra/activerecord/rake' require './public-bookmarks' [Ctrl-D]

Finally, we can include ActiveRecord in our app. We do this by adding the require statement to the top of public-bookmarks.rb . Also, we will query the DB using ActiveRecord to confirm that everything's in order:

# public-bookmarks.rb require 'sinatra' require 'sinatra/activerecord' get '/' do db_time = database.connection.execute('SELECT CURRENT_TIMESTAMP').first['now'] request.logger.info "DB time is #{db_time}" erb :hello_world end

In order to ensure our app works locally, we need to create the DB. We do so by running rake db:create before starting the app:

$ rake db:create $ rackup -p 4567

Now when you visit http://localhost:4567, you should see the following in your terminal output, confirming that the DB was successfully read:

D, [2014-09-14T23:38:54.719814 #1510] DEBUG -- : (0.2ms) SELECT CURRENT_TIMESTAMP I, [2014-09-14T23:38:54.719888 #1510] INFO -- : DB time is 2014-09-15 03:38:54.719593+00

This tells us that the PostgreSQL statement SELECT CURRENT_TIMESTAMP ran against the DB, and Sinatra was able to fetch the result via ActiveRecord.

3.2.1 Deploying to Heroku

With our Rails app, Heroku was able to detect the PostgreSQL dependency on its own and thus added it for us automatically. With our Sinatra app, we will need to add the Heroku addon ourselves. Fortunately, this is easy enough with a single command:

$ heroku addons:add heroku-postgresql

Once we do so, we can deploy by running git push heroku master . To confirm that the DB is being queried successfully, we can tail the Heroku log by running the following in its own terminal window:

heroku logs -t

Now, when we open the app in our browser window ( heroku apps:open ), you should see the following in your terminal output:

2014-09-15T04:15:03.694521+00:00 app[web.1]: I, [2014-09-15T04:15:03.694435 #2] INFO -- : DB time is 2014-09-15 04:15:03.694071+00

As we've seen, adding ActiveRecord to a Sinatra app is fairly painless, thanks in large part to the 'sinatra-activerecord' gem. Now we can implement basic CRUD functionality in both our Rails and Sinatra apps.

4 Implementing Basic CRUD

Here's a summary of the functionality we will be implementing:

Display a list (index) of public bookmarks. Display a single (show) public bookmark. Submit a new public bookmark (new/create). Destroy an existing public bookmark (destroy) -- password-protected.

In the above list, we have specified the REST requests that will be handled by the app in parenthesis (index, show, new, create, and destroy). For destroying bookmarks, we will implement HTTP Basic Authentication.

Specifically, a PublicBookmark object will have the following attributes (parenthesis indicate the data type):

title (string) url (string) description (text) submitter_email (string) created_at and updated_at timestamps (datetime)

4.1 Rails

Boilerplate code generation is a powerful feature of Rails that we will continue to leverage. In our case, we are actually able to generate most of the code we need by running a single 'rails generate' command. We also make sure to run the migration immediately after so that the schema change is included in the same commit as the migration script:

$ rails generate scaffold public_bookmark title:string url:string:uniq description:text submitter_email:string --no-stylesheets --no-helper --no-jbuilder --no-javascripts $ rake db:migrate

Ironically, the specified command options reduce the amount of boilerplate code generated, and we actually get more code generated that we need even with these options. Specifically, we need to remove the 'edit' functionality. We do so by:

Excluding the 'edit' and 'update' endpoints from the routes. Removing the controller actions. Deleting the unneeded view file. Implementing password protection on certain routes using HTTP Basic Auth.

4.1.1 Edit 'config/routes.rb`:

# config/routes.rb Rails.application.routes.draw do resources :public_bookmarks, except: [:edit, :update] root 'hello_world#index' end

4.1.2 Edit 'app/controllers/public_bookmarks_controller.rb':

# app/controllers/public_bookmarks_controller.rb (comments omitted) class PublicBookmarksController < ApplicationController before_action :set_public_bookmark, only: [:show, :destroy] def index @public_bookmarks = PublicBookmark.all end def show end def new @public_bookmark = PublicBookmark.new end def create @public_bookmark = PublicBookmark.new(public_bookmark_params) respond_to do |format| if @public_bookmark.save format.html { redirect_to @public_bookmark, notice: 'Public bookmark was successfully created.' } format.json { render :show, status: :created, location: @public_bookmark } else format.html { render :new } format.json { render json: @public_bookmark.errors, status: :unprocessable_entity } end end end def destroy @public_bookmark.destroy respond_to do |format| format.html { redirect_to public_bookmarks_url, notice: 'Public bookmark was successfully destroyed.' } format.json { head :no_content } end end private def set_public_bookmark @public_bookmark = PublicBookmark.find(params[:id]) end def public_bookmark_params params.require(:public_bookmark).permit(:title, :url, :description, :submitter_email) end end

rm app/views/public_bookmarks/edit.html.erb

View commit diff for app/views/public_bookmarks/index.html.erb and app/views/public_bookmarks/show.html.erb

At this point, we have almost all of our requirements implemented. The only functionality missing is the password protection on the 'public_bookmarks#destroy' action. A simple way to implement this is to use ActionController::HttpAuthentication::Basic .

4.1.4 Implement HTTP Basic Auth:

First, we create a Rails initializer that sets a global constant, which contains the name and password used for HTTP Basic Auth. This allows us to exclude production credentials from the source code, while allowing them to be loaded when the app is initialized.

$ cat > config/http_basic_auth.yml development: name: admin password: airpair production name: <%= ENV['HTTP_BASIC_AUTH_NAME'] %> password: <%= ENV['HTTP_BASIC_AUTH_PASSWORD'] %> [Ctrl-D] $ cat > config/initializers/http_basic_auth.rb HTTP_BASIC_AUTH = YAML::load(ERB.new(File.read("#{Rails.root}/config/http_basic_auth.yml")).result)[Rails.env] [Ctrl-D] $ heroku config:add HTTP_BASIC_AUTH_NAME=your-secret-production-name HTTP_BASIC_AUTH_PASSWORD=your-secret-production-password

Next, we modify the PublicBookmarks controller to authenticate for certain actions. Note that we add a controller action (index_authenticated). This allows us to have a view specifically designed for authenticated users (i.e. it will have destroy links).

# app/controllers/public_bookmarks_controller.rb class PublicBookmarksController < ApplicationController before_action :set_public_bookmark, only: [:show, :destroy] http_basic_authenticate_with name: HTTP_BASIC_AUTH['name'], password: HTTP_BASIC_AUTH['password'], only: [:index_authenticated, :destroy] def index_authenticated @public_bookmarks = PublicBookmark.all @authenticated = true render :index end ... end # config/routes.rb Rails.application.routes.draw do resources :public_bookmarks, except: [:edit, :update] do get :index_authenticated, on: :collection end root 'hello_world#index' end

Finally, we add a conditional in the index view to display the destroy link only if the @authenticated flag is set.

# app/views/public_bookmarks/index.html.erb ... <tr> <td><%= public_bookmark.title %></td> <td><%= public_bookmark.url %></td> <td><%= public_bookmark.description %></td> <td><%= public_bookmark.submitter_email %></td> <td><%= link_to 'Show', public_bookmark %></td> <% if @authenticated %> <td><%= link_to 'Destroy', public_bookmark, method: :delete, data: { confirm: 'Are you sure?' } %></td> <% end %> </tr> ...

After we deploy to Heroku (by running git push heroku master ), we will need to run the pending migration (which creates the public_bookmarks table). We also restart the Heroku app for good measure:

$ heroku run rake db:migrate $ heroku ps:restart

4.1.5 Summary

After committing and pushing the code to Heroku, we have basic CRUD functionality implemented and deployed on our Rails app. Here's what was involved:

20 lines of code changed (237 including boilerplate, 26 lines removed); 8 files added or modified (12 including boilerplate); and 5 commands executed (not including cat , rm , etc.)

Now we move on to Sinatra and compare the process.

4.2 Sinatra

Unlike Rails, we do not have code generation scripts we can rely on, so we will need to write our code from scratch. Although there are scaffold solutions for Sinatra (notably the Padrino framework), we have chosen not to explore these here so as to not deviate too far from a pure Sinatra implementation.

4.2.1 Build Model

Generate:

$ rake db:create_migration NAME=create_public_bookmarks

Write:

# db/migrate/YYYYMMDDHHMMSS_create_public_bookmarks.rb class CreatePublicBookmarks < ActiveRecord::Migration def change create_table :public_bookmarks do |t| t.string :title t.string :url t.text :description t.string :submitter_email t.timestamps end add_index :public_bookmarks, :url, unique: true end end

Run:

# rake db:migrate

Note: After you commit and push to Heroku, make sure you run heroku run rake db:migrate so that migrations run on the production environment as well.

4.2.2 Create ActiveRecord Model Class

$ mkdir models $ cat > models/public_bookmark.rb class PublicBookmark < ActiveRecord::Base end [Ctrl-D]

We also need to explicitly load the model in our main app ( public-bookmarks.rb ):

# public-bookmarks.rb ... require 'sinatra/activerecord' require './models/public_bookmark' ...

4.3 Build Controllers

4.3.1 Add Rack::Flash

We will make use of session-based flash messages to pass messages to the user from the controllers to the view (e.g. upon the successful creation of a public bookmark). While Rails comes with this built-in, this needs to be added onto our Sinatra installation.

To do so, we install the rack-flash3 gem by adding it to the Gemfile (just before the last line of the file). Then, we run bundle install :

# Gemfile ... gem 'rack-flash3' ...

$ bundle install

Lastly, we include and initialize Rack::Flash and enable session in the main app by adding it to public-bookmarks.rb (immediately after our last require ):

# public-bookmarks.rb ... require './models/public_bookmark' require 'rack-flash' enable :sessions use Rack::Flash ...

4.3.2 Add Non-Passworded Controllers

Append the following to the end of the public-bookmarks.rb file

# public-bookmarks.rb ... erb :hello_world end get '/public_bookmarks' do @public_bookmarks = PublicBookmark.all erb :'public_bookmarks/index' end get '/public_bookmarks/new' do @public_bookmark = PublicBookmark.new erb :'public_bookmarks/new' end get '/public_bookmarks/:id' do @public_bookmark = PublicBookmark.find(params[:id]) erb :'public_bookmarks/show' end post '/public_bookmarks/create' do @public_bookmark = PublicBookmark.new(params[:public_bookmark]) if @public_bookmark.save flash[:notice] = 'Public bookmark successfully created!' redirect to("public_bookmarks/#{@public_bookmark.id}") else erb :'public_bookmarks/new' end end

4.3.3 Add HTTP Basic Auth Support

First, we create an initializer which will populate a global constant, which contains the authentication credentials our app will check against. We do this so that we can define different credentials per environment, and so that production credentials can be defined outside of the source code (this is done via the last command we execute below):

$ cat > config/http_basic_auth.yml development: name: admin password: airpair production: name: <%= ENV['HTTP_BASIC_AUTH_NAME'] %> password: <%= ENV['HTTP_BASIC_AUTH_PASSWORD'] %> [Ctrl-D] $ mkdir config/initializers $ cat > config/initializers/http_basic_auth.rb HTTP_BASIC_AUTH = YAML::load(ERB.new(File.read('./config/http_basic_auth.yml')).result)[settings.environment.to_s] [Ctrl-D] $ heroku config:add HTTP_BASIC_AUTH_NAME=your-secret-production-name HTTP_BASIC_AUTH_PASSWORD=your-secret-production-password

To ensure the HTTP_BASIC_AUTH constant is loaded, we require it in public-bookmarks.rb :

# public-bookmarks.rb ... require 'rack-flash' require './config/initializers/http_basic_auth.rb' ...

Now, we include the helper methods which will be used by the controllers for authentication (taken from Sinatra FAQ > How do I use HTTP auth?). We add this near the top of public-bookmarks.rb ; after the require statements but before the routes:

# public-bookmarks.rb ... use Rack::Flash helpers do def protected! return if authorized? headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' halt 401, "Not authorized

" end def authorized? @auth ||= Rack::Auth::Basic::Request.new(request.env) @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == [HTTP_BASIC_AUTH['name'], HTTP_BASIC_AUTH['password']] end end get '/' do ...

4.3.4 Add Passworded Controllers

Add the following before the non-passworded controllers we added in part 2:

# public-bookmarks.rb ... erb :hello_world end get '/public_bookmarks/index_authenticated' do protected! @authenticated = true @public_bookmarks = PublicBookmark.all erb :'public_bookmarks/index' end post '/public_bookmarks/destroy/:id' do protected! @public_bookmark = PublicBookmark.find(params[:id]) @public_bookmark.destroy redirect to("public_bookmarks/index_authenticated") end get '/public_bookmarks' do ...

As with defining routes in any web application, the order is important. We want to include all the specific routes at the top, while the less-specific routes are declared nearer to the bottom of the file. This is done in order to avoid having the wrong route handle a given request. For example, if we were to place the get 'public_bookmarks/index_authenticated' route after the get 'public_bookmarks/:id' route, then an HTTP GET request to /public_bookmarks/index_authenticated would be intercepted by the get 'public_bookmarks/:id' route (i.e. the the get 'public_bookmarks/index_authenticated' would never be reached).

Rails encourages the use of resourceful routes as a way to mitigate these situations. Resourceful routing solutions for Sinatra do exist. In particular, the Sinatra Rabbit gem provides an expressive DSL for building REST-based APIs (effectively, this addresses the resourceful routing issue within the domain of APIs). For non-API use cases, the Sinatra::Resourceful gem aims to provide resourceful routing functionality similar to that which is provided by Rails. Having said that, I haven't had a chance to explore that gem yet.

4.4 Build Views

Now that we have our models and controllers set up in our Sinatra app, we can add in the views. There is nothing particularly of note here, other than it is a bit of a tedious process. Personally, I find having auto-generated boilerplate views helpful, at least as a starting point.

views/public_bookmarks/index.erb:

<span>Listing public_bookmarks</span> <p id="notice"><%= flash[:notice] %></p> <table> <thead> <tr> <th>Title</th> <th>Url</th> <th>Description</th> <th>Submitter email</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @public_bookmarks.each do |public_bookmark| %> <tr> <td><%= public_bookmark.title %></td> <td><%= public_bookmark.url %></td> <td><%= public_bookmark.description %></td> <td><%= public_bookmark.submitter_email %></td> <td> <a href="/public_bookmarks/<%= public_bookmark.id %>"> Show </a> </td> <% if @authenticated %> <td> <form action="/public_bookmarks/destroy/<%= public_bookmark.id %>" method='post'> <input type='submit' value='Destroy' onclick="return confirm('Are you sure?')"> </form> </td> <% end %> </tr> <% end %> </tbody> </table> <br> <a href='/public_bookmarks/new'> New Public Bookmark </a>

views/public_bookmarks/show.erb:

<p id="notice"><%= flash[:notice] %></p> <p> <strong>Title:</strong> <%= @public_bookmark.title %> </p> <p> <strong>Url:</strong> <%= @public_bookmark.url %> </p> <p> <strong>Description:</strong> <%= @public_bookmark.description %> </p> <p> <strong>Submitter email:</strong> <%= @public_bookmark.submitter_email %> </p> <a href='/public_bookmarks'> Back </a>

views/public_bookmarks/new.erb:

<span>New public_bookmark</span> <%= erb :'public_bookmarks/form' %> <a href='/public_bookmarks'> Back </a>

Note the slightly different syntax used in Sinatra when referencing partials ( <%= erb :'public_bookmarks/form' %> ). Also note that Sinatra does not use the Rails convention of expecting partial file names to begin with an underscore.

views/public_bookmarks/form.erb:

<form action="/public_bookmarks/create" method='post'> <% if @public_bookmark.errors.any? %> <div id="error_explanation"> <span><%= pluralize(@public_bookmark.errors.count, "error") %> prohibited this public_bookmark from being saved:</span> <ul> <% @public_bookmark.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <label for="public_bookmark_title">Title</label><br> <input id="public_bookmark_title" name="public_bookmark[title]" type="text"> </div> <div class="field"> <label for="public_bookmark_url">Url</label><br> <input id="public_bookmark_url" name="public_bookmark[url]" type="text"> </div> <div class="field"> <label for="public_bookmark_description">Description</label><br> <textarea id="public_bookmark_description" name="public_bookmark[description]"></textarea> </div> <div class="field"> <label for="public_bookmark_submitter_email">Submitter email</label><br> <input id="public_bookmark_submitter_email" name="public_bookmark[submitter_email]" type="text"> </div> <div class="actions"> <input type="submit" value="Create Public bookmark"> </div> </form>

Another point to note when building views in Sinatra: we are not able to leverage form and link helper methods. These are particularly useful when we don't have the URL structure of our web app quite defined yet. For example, with Rails, they dynamically generate the HTML for URLs and forms based on the config/routes.rb file. Without this, we must hard-code all of our URLs in our views, which means a potential headache if we need to later restructure the URL hierarchy (which is quite possible given the effect this has on factors such as SEO and API design). On the other hand, this does not tend to change much once the web app is live for the same reasons (changing routing leads to 404s, which are undesirable for both webpages indexed by a search engine as well as an actively-used API).

4.5 Summary

Similar to how we did in the other scenarios, let's add up and summarize what was involved in getting our basic CRUD functionality implemented on Sinatra and deployed on Heroku:

215 lines of code added; 12 files added or modified; and 3 commands executed.

5 Closing Thoughts

Sinatra was a clear winner when implementing a basic 'Hello World' web app. However, we all know that such an application is a hypothetical one; a more practical test was to implement the basic CRUD functionality. In this case, Rails came out on top mainly thanks to its boilerplate code generation. Furthermore, some code scalability issues were identified with Sinatra, in particularly in terms of how routes are managed as well as the lack of view helpers (for generating HTML for URLs and forms dynamically).

Having said that, the 'CRUD' test is biased towards Rails; being an MVC framework it is designed and optimized for such a use case. What are the use cases where Sinatra wins over Rails? More custom web application prototypes such as custom APIs, or static websites which require limited dynamic behavior are likely to be more suitable for Sinatra.

Padrino is a popular web applications framework built on top of Sinatra. It's something to consider if you already have a web app built with Sinatra that is growing in complexity, or if you're looking to build one from scratch but are biased towards Sinatra for whatever reason.

6 Reference (repos)