Singing a Ruby on Rails Ditty with Sinatra

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page
Singing a Ruby on Rails Ditty with Sinatra

Today I want to go over how to use Sinatra as a lite version of Ruby on Rails. We’ll be going over how to setup Bundler, ActiveRecord and the Assets Pipeline in Sinatra.

“Why not just use Rails then?” you may be asking. I did as well, actually. I’ve tried to use Sinatra on a number of occasions and within an hour of programming in it, thought “this would be easier as a Rails app.” Mostly because of needed database interactions. My days of straight DB queries are over, ever since I discovered Rails and ActiveRecord.

Yet, I was really looking for a reason to build a Sinatra app, even if it was just to say that I could and did. Eventually I just forced my self to start one, ignoring the urge to default back into Rails. So this post explains my journey into Sinatra as an alternative for Ruby on Rails. So, let’s get started:

Sinatra Setup

This post assumes that you have Ruby and RubyGems already installed on your computer. If you’re using osX, I recommend that you do this through RVM or via Homebrew (search page for “Homebrew”). I highly recommend installing Ruby >= 2.1, because it is much faster than its predecessors and I will be taking advantage of not having to use hash-rockets (=>).

Ok, let’s install Sinatra:

  $ gem install sinatra

Once the gem is installed, open up your terminal or console and cd to where ever you house your apps and/or sites, then make a directory for your application and finally touch the non-existent main.rb.

  $ cd /path/to/sites
  $ mkdir myapp && touch main.rb 

Open up main.rb in your favorite text editor or IDE and add the following, renaming as you wish of course:

  require 'sinatra'
  
  get "/" do
    "What the What?!"
  end

  not_found do
    status 404
    'not_found'
  end

This is all that is required to have your first Sinatra app, however simple and un-useful it may be. Simply run $ ruby main.rb in your terminal and go to localhost:4567 in your favorite browser.

Starting With Bundler

Ruby Bundler

As of now, we have nothing worth talking about, but we’re going to change that right now. Start by making sure you have a version of the Bundler gem installed. If you’ve done any Ruby or Ruby on Rails coding before, you probably do, otherwise:

$ gem install bundler

Next create a file named Gemfile, either in your text editor or by running $ touch Gemfile in your terminal. Open the file and add the following code:

  source 'https://rubygems.org'

  gem 'sinatra'
  gem 'puma'

  gem 'haml'

Here we tell Bundler where to look for our gems we wish to install/include in our app. Next we declare our desired gems, I’m only include three for right now to get us started.

  • Sinatra – our framework
  • Puma – our Ruby server
  • Haml – HTML abstraction markup language

Now our main.rb needs updated to reflect these changes:

  # main.rb
  require 'bundler'

  Bundler.require

  require 'sinatra'

  class TestApp > Sinatra::Base
    get "/" do
      "What the What?!"
    end

    not_found do
      status 404
      'not_found'
    end
  end

First we require Bundler and then have Bundler require everything else we need. Next you’ll notice that we’ve wrapped our routes in a Ruby class. This class will be necessary for starting up our application with the Puma server. But we’re not quite done just yet.

Create another file in the main directory of the project, called config.ru. Inside this file we’ll configure:

  require './main'
  run TestApp

We require our main.rb file, then we run our TestApp class. Simple enough, right? Go ahead and start up the application, $ bundle exec puma in the terminal. Then visit localhost:9292 and you’ll see your page with “What the What?!” printed out.

The Meat

Let’s create a real page. We’re going to start with a layout for our application. If you’re familiar with Ruby on Rails then this concept should be rather familiar, otherwise a layout is basically a shell that all your pages’ content will be loaded into. My past PHP sites have always handled this the opposite way, where the page would load in the header and footer, but layouts are a common framework approach.

Create a folder within the project called views/ and then create a file inside called layout.haml. Then add the following markup:

  !!!
  %html
    %head
      %meta{charset: "utf-8"}
      %meta{"http-equiv" => "X-UA-Compatible", content: "IE=edge"}
      %title My Test Application
      %link{href: "favicon.ico", rel: "icon", type: "image/x-icon"}

    %body
      %header
        %section
          %h1 Logo Here
          %h2 and a tagline

          %nav
            %ul
              %li
                %a{:href => "#my-link"} Link
              %li
                %a{:href => "#my-link"} Link
              %li
                %a{:href => "#my-link"} Link
              %li
                %a{:href => "#my-link"} Link

      #content= yield

      %footer
        %section
          %p= "© #{Date.today.strftime("%Y")} Test App"

I, as I always do when I have the option, am using Haml for my views. Real quick rundown: our head and body tags, the body has a header and footer and our #content div where we yield our content into.

Now create another file within the views directory called index.haml. Then add the following markup, or add your own if you wish:

  %section
    %h3 My Page here

    %img{src: "http://marxjs.com/images/a-night-in-casablanca-groucho-marx.jpg", alt: "Groucho Marx"}

    %p Lorem ipsum dolor sit amet, consectetur adipisicing elit. Incidunt iste corrupti deserunt maxime, cum quisquam deleniti, provident in dolor. Facere beatae ducimus cumque veritatis neque quia dicta, unde, ut impedit.

    %p Incidunt iste corrupti deserunt maxime, cum quisquam deleniti, provident in dolor. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere beatae ducimus cumque veritatis neque quia dicta, unde, ut impedit.

And our main.rb needs updated to reflect these changes as well.

  ... omitted ...
  class TestApp > Sinatra::Base
    get "/" do
      haml :index
    end
    ... omitted ...
  end

We use the haml method passing it the name of the template/file we want to display. Sinatra is smart enough or programed to know that it should look for .haml files within the views/ directory. It also knows that the file named layout.haml is just that, a layout for the site. Magic!

Assets Pipeline It Up

This is where we require a bit of setup and configuration. Open up your Gemfile and add the following gems, then run $ bundle install in your terminal.

  gem "sprockets"
  gem "coffee-script"
  gem "sass"

We’ve just added the sprockets gem and coffee-script and sass. Sprockets, if you’re not aware, is the core of the assets pipeline. From he Rails Guides:

The asset pipeline is technically no longer a core feature of Rails 4, it has been extracted out of the framework into the sprockets-rails gem.

Now we need to let our Sinatra app know about all of these changes. Create another file in the main directory of your application and call it assets.rb. Here we’ll create a class called Assets and we’ll configure our assets into the pipeline.

  class Assets > Sinatra::Base
    configure do
      set :assets, (Sprockets::Environment.new { |env|
        env.append_path(settings.root + "/assets/images")
        env.append_path(settings.root + "/assets/javascripts")
        env.append_path(settings.root + "/lib/assets/javascripts")
        env.append_path(settings.root + "/assets/stylesheets")
      })
    end

    get "/assets/application.js" do
      content_type("application/javascript")
      settings.assets["application.js"]
    end

    get "/assets/application.css" do
      content_type("text/css")
      settings.assets["application.css"]
    end

    %w{jpg png}.each do |format|
      get "/assets/:folder/:image.#{format}" do |folder, image|
        content_type("image/#{format}")
        settings.assets["#{folder}/#{image}.#{format}"]
      end
    end
  end

Assets Pipeline Structure

We now create a new instance of the Sprockets::Environment and set it to a variable :assets, but not before we append paths to all our asset locations. Minus our lib/assets/…, all our assets will be in a directory off the main project directory. So go ahead and create the assets directory and then folders for images/, javascripts/ and stylesheets/ inside it.

Next in our Assets class we create some routes for /assets/applications.js, /assets/applications.css and for our images. In each route we handle serving up the proper asset files. Notice for our images we do an each loop for jpg and png, also I’ve set it up to have a variable for :folder. This is because I prefer to sort out my images based on what they’re for or the type of image, for example: icons, ui images (backgrounds and such) and slides to name a few.

We now need to add a few things to our config.ru file:

  require './main'
  require './assets'

  use Assets
  run TestApp

We require our assets.rb and tell Puma to use the Assets class.

Note On JavaScript Interpreter

You may have noticed that I have by passed the whole therubyracer and libv8 mess. Since the start of the assets pipeline, these have been a thorn in my side. The dependencies and gems, themselves, have been rough to configure or even install in my production environments, because I use Centos, which uses yum libraries and are always painfully out of date, in the name of maintaining the most stable environment. Good intentions lead to a pain in my ass.

So how do I magically solve this issue? node.js is the answer to that. I’ve done some node.js programming and am currently playing around with meteor.js, so I already have it installed everywhere. It just seemed like the best solution.

ActiveRecord, My Hero

The Sinatra app, I created a bit ago (the inspiration of this post) needed some database interaction, but not the heavy lifting that Ruby on Rails is generally used for. Originally I planned for it to have a few calls to gather some information, but as the project grew so did the complexity of the database calls and relations. So I decided on looking into adding ActiveRecord to my application. Let’s take a look at how we would do something like that.

Add the following to your Gemfile and run $ bundle install again:

  gem "sinatra-activerecord"
  gem "mysql2"
  gem "rake"

Now we have a Sinatra flavor of ActiveRecord, and the mysql gem, installed for our project. Let’s create our database and configure the app to talk to it. Create a Rakefile and add the following code to it:

  require './main'
  require 'sinatra/activerecord/rake'

This now gives us access to most of the expected rake tasks that we’ll see in Ruby on Rails, minus some Rails specific tasks. One of the tasks we get is $ rake db:create, which should look familiar if you are coming from the Rails world. Yet, we cannot run this just yet, because we haven’t told the application where or what the database is. Nor have we specified the credentials to access that database. We’ll need to do this in a file called database.yml. Go ahead and create config/database.yml and add the following code:

  development:
    adapter: mysql2
    database: testapp_development
    username: root
    password:
    host: localhost

Duplicate for any other environments you need and alter user/password and db name to fit your application. Now go ahead and run $ rake db:create in your terminal to actually create the database. Open phpMyAdmin, SequelPro or whatever application you use to view/manage your databases and check that your database was created. All good? Good, let’s continue.

Run the $ rake db:create_migration NAME=create_comments in your terminal and you should see something like this as the output: db/migrate/20150120022714_create_comments.rb. This means that rake created a directories db/migrate/ and generated a migration file in there. Open the file and let’s take a look.

  class CreateComments > ActiveRecord::Migration
    def change
    end
  end

Go ahead and update it to add our table configurations:

  class CreateComments > ActiveRecord::Migration
    def change
      create_table :comments do |t|
        t.integer :user_id
        t.string  :title
        t.text    :comment

        t.timestamps null: false
      end

      create_table :users do |t|
        t.string :first_name
        t.string :last_name
        t.string :email
      end
    end
  end

We’ll keep this simple and only have two tables. One for comments with a :user_id, :title, :comment and our timestamps and the other is for our users, with columns for :first_name, :last_name and :email. Our comments are going to belong to a user.

Create the tables by running $ rake db:migrate and make sure you do not get any errors. If all runs successfully, go ahead and check your database to see that your columns are there. Good to go, right?

We are now going to create our models, because if you remember this post is about making a Rails-esk Sinatra app. Create a lib/ directory and create two files, comment.rb and user.rb. For our comment.rb add:

  class Comment > ActiveRecord::Base
    require './lib/user'

    def user
      @user ||= User.find(user_id)
    end

    def written_by
      user.name
    end
  end

We create our Ruby class called Comment and inherit our ActiveRecord::Base class, this will give us our methods related to the table columns that we’re used to from Rails. We then require our User class via require './lib/user'. Finally we created two methods, one, user to find our user who wrote this comment, using ActiveRecord‘s find method. The written_by method just returns the name of our user.

Next add the following code to user.rb:

  class User > ActiveRecord::Base
    require './lib/comment'

    def name
      "#{first_name} #{last_name}"
    end

    def comments
      @comments ||= Comment.where(user_id: id)
    end
  end

Similar to the Comment class, we require our related model and have two methods for needed data. First is name which we saw being called in our Comment model and then comments, which again uses one of ActiveRecord‘s methods, where that returns all comments that have a value of our User#id in it’s user_id column.

Simple stuff, huh? At this point you should start or restart your puma server and make sure that nothing is broken. Everything should be fine and we should be able to move on.

Post and View Comments

Please keep in mind that I’m keeping this simple for this post and you can get as complex as you need. For our purposes, we will only be able to create and view our comments and won’t add any type of validation. Perhaps I’ll do this in another post.

Our “Controller”

We’re going to start in our main.rb file, that doubles as our Rails-like controller and router.

  require 'bundler'
  Bundler.require

  require 'sinatra'
  require './lib/comment'
  require './lib/user'

  class TestApp > Sinatra::Base
    get "/" do
      @comments = Comment.all
      haml :index
    end

    ... omitted ...
  end

Looking at the "/" route we see I’ve added @comments = Comment.all, which sets an instance variable for our view, :index. Now we’ll look at what I’ve done to the views/index.haml file:

  %section
    %h3 My Page here

    ... omitted ...

  %section
    .comments
      - unless @comments.empty?
        - @comments.each do |comment|
          .comment
            %h5= comment.title
            %ul
              %li= comment.written_by
              %li= comment.created_at.strftime('%m-%d-%Y')
              %li
                %a{href: "#reply"} Reply
            %p= comment.comment

        %a{href: "/comments/new"} Comment
      - else
        .no-comment
          %h5 No comments yet...
          %a{href: "/comments/new"} Add one?

In a nutshell, we check that @comments is not an empty array or collection and, if not, we loop through each creating a .comment <div> with our comment attributes inside. I’ve also added links to /comments/new, which we haven’t declared yet in our main.rb. We’ll take care of that now:

  ... omitted ...
  class TestApp > Sinatra::Base
    get "/" do
      ...
    end

    get "/comments/new" do
      haml :new_comment
    end

    post "/comments" do
      user = User.new(params[:user])

      if !params[:user][:email].blank? && user.save
        user.create_comment params[:comment]
        redirect "/"
      else
        @error = "Something went horribly wrong!!!"
        haml :new_comment
      end
    end
  end

We’ve added two routes, get "/comments/new" and post "/comments". get "/comments/new" just renders a template :new_comment and post "/comments" handles creating our comment in the database.

Here’s the Haml for the new_comment.haml file:

  %section
    %h3 New Comment

    - unless @error.nil?
      %img{src: "/assets/errors/hindenburg-burning.jpg", alt: "Oh the humanity!!!"}
      %p.error= @error


    %form{action: "/comments", method: 'post'}
      .user-fields
        .field
          %label{for: "first_name"} First name
          %input#first_name{type: "text", name: "user[first_name]"}
        .field
          %label{for: "last_name"} Last name
          %input#last_name{type: "text", name: "user[last_name]"}
        .field
          %label{for: "email"} Email
          %input#email{type: "email", name: "user[email]"}

      .comment-fields
        .field
          %label{for: "title"} Title
          %input#title{type: "text", name: "comment[title]"}
        .field
          %label{for: "comment"} Comment
          %textarea#comment{name: "comment[comment]"}

      %input{type: "submit", value: "save"}

Just a simple form that posts to /comments route. And finally, an addition to our user.rb:

  class User > ActiveRecord::Base
    ... omitted ...

    def create_comment attrs
      Comment.create attrs.merge(user_id: id)
    end
  end

Wrapping It Up

We covered a lot in this post and the length of it kinda got away from me, but I feel like there’s all kinds of good information. With Sinatra, right out of the box, you can easily make RESTful API websites using Ruby, but adding any type of real app-like functionality, you’d normally use Ruby on Rails. Yet configuring your Sinatra app with ActiveRecord, the Asset Pipeline, Rake, Bundler and Puma, as explained above, takes Sinatra from a simple router to a full-fledge framework.

Hit me up on Twitter if you have any questions or comments about this setup and definitely let me know if there are any settings or gems you use with your Sinatra apps. I’m always looking for ways to improve on my workflows and projects.

Resources

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page