Frank on Fire: Getting Started with Sinatra and Ember-CLI

Here at Bendyworks, we've noticed Ember.js's growing adoption in the front-end community and have found that it strikes a good balance between flexibility and adherence to battle tested architectural practices.

Ember-CLI takes things one step further by standardizing on a certain set of build tools and application structure, allowing you to get on with developing your application rather than writing boilerplate. And the announcement in the Roadmap to Ember 2.0 that Ember-CLI will become a "first class part of the Ember experience" is all the more reason to start using it now.

Although Ember is impressively backend agnostic, the de-facto choice for serving Ember apps and providing APIs has generally been nodeJS. Using one language on both client and server, but in this post I'm going to talk about a different option: using Ruby and Sinatra as the backend.

I won't spend too long arguing for the value of Sinatra vs Node for the backend, but I think there are some worthy points in Ruby's favor. First and foremost, Ruby has a diverse set of tools for interacting with databases of all kinds, and has particularly good support for SQL databases courtesy of Rails and ActiveRecord. While there are ORM options for node, none have the maturity (or, in my opinion, elegance and ease of use) of the options on the Ruby side. Second, Ruby makes it very easy to get up and running with a prototype application, and Sinatra takes this attitude and applies it to web development. It also has a huge ecosystem of mature and well supported libraries ("gems" for the non-rubyist) that can be used to quickly add features to any application, making prototyping even easier. In short, the language has incredible whipupitude, which is just what we need for building a simple backend for an Ember application.

Setup

Before getting started on our app, we'll need to install a few prerequisites:

  1. npm (comes installed with Node.js)
  2. Bower
  3. ember-cli
  4. Ruby (I use RVM with MRI Ruby)
  5. Bundler

Once these are set up, we'll be able to manage installing libraries and any future dependencies via Bower and npm.

Installing npm can be a little tricky, but since it comes with Node.js I'd suggest just installing that if you're unsure where to start.

Once you have npm, the rest of the dependencies are easy to install:

Bower

npm install -g bower

Ember-CLI

npm install -g ember-cli

Ruby

If you're following along on a Mac, you already have Ruby installed, so you can skip this step! If not, you have a few different options to install Ruby. One good choice is RVM, which can be installed along with the current stable version of MRI Ruby by running

\curl -sSL https://get.rvm.io | bash -s stable

Bundler

Bundler can be installed as a Ruby gem

gem install bundler

A Star Is Born - Generating a new Ember application

Once all the dependencies are installed, its time to generate a skeleton for our application! For the purposes of this tutorial, lets call our app newsboy_blues, and generate it using the "ember new" command

ember new newsboy_blues

Now you can switch into the new newsboy_blues directory and check your setup by running ember server and visiting your application at http://localhost:4200/. You should see "Welcome to Ember.js" if everything is set up properly.

Sinatra Takes The Stage - Setting up the server

Next we'll create a basic Sinatra application to serve up our application. For now, this may seem like extra work compared to using the node server that comes with ember-cli, but we'll begin to see the advantages of using Ruby on the server when it comes time to access the database.

To start, create a file called server.rb with the following contents:

# server.rb
require 'sinatra'

configure do
  set :public_folder, File.expand_path('dist')
end

get '*' do
  send_file 'dist/index.html'
end

and another file called Gemfile that we'll use to install Ruby libraries

# Gemfile
source 'https://rubygems.org'

gem 'sinatra'

Then have bundler install Sinatra by running

bundle install

Finally, we'll need to compile the javascript for our application. Although our Sinatra application won't do this automatically, ember-cli includes a command to watch for changes to our project and automatically avenge when they happen. Open up a new terminal, and run

ember build --watch

Now all we have to do is start the application

ruby server.rb

And we can again see "Welcome to Ember.js" when visiting http://localhost:4567. Note the change of port; by default Sinatra runs on 4567.

Start Spreadin' The News - Building the client using Fixtures

Now that all the groundwork is laid, we can really begin building a news reader. In the end we'll be able to pull news from any RSS feed using our server, but to begin we'll just use fixture data so we can focus on building the interface.

To get started we can use ember-cli's generate command to generate scaffolds for the routes, models, and templates we'll be creating. First we generate a model we can use to represent a news story

ember generate model story

Next we generate a route which we'll use to link our story model to a view template

ember generate route stories

we'll also need to create our own application adapter to use fixture models

ember generate adapter application

and an index route so we can redirect to our main 'stories' route

ember generate route index

With all of this finished, we can fill in some simple behavior for our nascent news app. First we'll change app/adapters/application.js to tell ember to use the FixtureAdapter adapter for loading models

// app/adapters/application.js
import DS from 'ember-data';

export default DS.FixtureAdapter.extend({
});

Then set up app/routes/index.js to redirect to our stories route

// app/routes/index.js
import Ember from 'ember';

export default Ember.Route.extend({
  redirect: function(){ this.transitionTo('stories'); }
});

Next we'll set up our Story model in app/models/story.js, defining a few basic attributes as well as some fixtures.

// app/models/story.js
import DS from 'ember-data';

var Story = DS.Model.extend({
  title: DS.attr('string'),
  url: DS.attr('string'),
  story_content: DS.attr('string')
});

Story.reopenClass({
  FIXTURES: [
    { id: 1,
      title: 'Local Web Consultancy Fights Off Alien Invasion, Saves Kitten',
      url: 'http://www.bendyworks.com',
      story_content: 'Continuing their streak of epic daring do, crime fighting outfit Bendyworks has...' },
    { id: 2,
      title: 'Teach Your Cat To Fly With This One Weird Trick',
      url: 'http://en.wikipedia.org/wiki/Anti-gravity',
      story_content: "You'll never believe..." }
  ]
});

export default Story;

And update app/routes/stories.jsto load our Story fixtures as the model for the 'stories' route

// app/routes/stories.js
import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.find('story');
  }
});

Last but certainly not least, we'll edit the 'stories' template in app/templates/stories.hbs to define how our stories are displayed. Note the use of the 'triple handlebar' for story_content to prevent Handlebars from HTML escaping the contents of a story, which usually has HTML formatting. A word of warning though; mixing in content from unknown sources is a serious security issue. Eventually you'd want to sanitize the contents of each story on the server side, but since we're just using fixtures that we wrote there's no need to worry about it for now.

<--! app/templates/stories.hbs -->
<ul>
{{#each model}}
<li><h4><a {{bind-attr href=url}}>{{title}}</a></h4></li>
<p>{{{story_content}}}</p>
{{/each}}
</ul>

And now we can see our exciting news stories displayed at http://localhost:4567! If you don't see any change, make sure you're running ember build --watch, and that its output isn't showing any errors.

Something Wonderful - Using gems to make Database and RSS access a breeze

Up until this point we haven't been using our Sinatra app for anything other than serving up the main Ember application. Since we had to go through a few (small) contortions to get this right, you may be wondering where Sinatra adds value in this application. The answer is in Ruby's excellent library support for all sorts of server side operations. In this case, database access and RSS parsing.

For collecting news stories to display in our ember app, we'll use the Feedjira gem, which provides a simple interface for fetching and parsing RSS feeds. To store and persist our stories, we'll be using the sinatra-activerecord gem with sqlite. This will let us get up and running faster, but if we ever want to handle large numbers of stories or deploy our application to Heroku it will be fairly simple to switch to PostgreSQL.

To get started, add the 'feedjira' and 'sinatra-activerecord' gems to your Gemfile, as well as 'sqlite3' and 'rake' which are required by sinatra-activerecord.

# Gemfile
source 'https://rubygems.org'

gem 'sinatra'

gem 'feedjira'

gem 'sinatra-activerecord'
gem 'sqlite3'
gem 'rake'

Next, require the new libraries inside server.rb, and add a line to the configuration block telling the app which database to use.

# server.rb
require 'sinatra'
require 'sinatra/activerecord'
require 'feedjira'

configure do
  set :public_folder, File.expand_path('dist')
  set :database, {adapter: "sqlite3", database: "news.sqlite3"}
end

get '*' do
  send_file 'dist/index.html'
end

Finally, create a Rakefile and require the sinatra-activerecord tasks and the main application.

# Rakefile
require "sinatra/activerecord/rake"
require "./server"

Now just run bundle install and rake db:create. If it returns successfully the database is setup and ready for us to start defining the tables that will go in it.

To begin building a simple "Stories" model and database table, run rake db:create_migration NAME=create_stories. Fill out the migration as follows

class CreateStories < ActiveRecord::Migration
  def change
    create_table :stories do |t|
      t.string :title
      t.text :story_content
      t.string :url
    end
  end
end

Sinatra won't generate a model file for us like Rails does, so let's create an empty one in lib/models/story.rb.

# lib/models/story.rb
class Story < ActiveRecord::Base
end

You'll also need to load the model by adding require_relative 'lib/models/story' to your main server.rb file

Then run rake db:migrate, and we're ready to populate our stories table with data from an RSS feed! Although a fully featured news reader would probably have some concept of a "Feed" which could be used to organize stories, our simple ember app only models Stories, so we won't bother creating a model for Feeds.

Since we already have a Rakefile, lets just add another task to fetch all new stories from a feed for now.

# Rakefile
require "sinatra/activerecord/rake"
require "./server"

desc "fetch stories from RSS feed"
task :fetch_stories, [:url] do |t, args|
  Story.fetch_from(args[:url])
end

And then implement the fetch_from(url) function in the Story model.

# lib/models/story.rb
class Story < ActiveRecord::Base
  def self.fetch_from(url)
    f = Feedjira::Feed.fetch_and_parse(url)

    f.entries.each do |e|
      unless Story.find_by(url: e.url).present?
        Story.create(title: e.title,
                     url: e.url,
                     story_content: e.content)

        puts "Story #{e.title} added!"
      end
    end
  end
end

Now you should be able to run

rake fetch_stories[http://bendyworks.com/feed/]

and see a list of stories such as

Story The Iconic Madison – Free Icon Set added!
Story 2014 Rails Rumble added!
Story The Old and the New: SOAP and Ember.js added!
Story Two Keynote tips that everyone should know added!
Story Transducers: Clojure’s Next Big Idea added!
Story Why We Can’t Wait for Madison+ Ruby added!
Story BendyConf: Tech Education for the Rest of Us added!
Story Tessel: A First Look at JavaScript on Hardware added!
Story Externally Embedding Ember added!
Story BendyConf: A Paean To Plain Text added!

Indicating that everything worked, and some stories have been added to our database.

Can't We Be Friends? - Integrating server and client

All that's left to do now is set up our Ember application to fetch and display these stories. Because we've already developed the front end using fixtures, we don't need to worry about any more template changes or controller logic.

First, add a route to server.rb that will return the stories in our database

# server.rb
require 'sinatra'
require 'sinatra/activerecord'
require 'feedjira'

require_relative 'lib/models/story'

configure do
  set :public_folder, File.expand_path('dist')
  set :database, {adapter: "sqlite3", database: "news.sqlite3"}
end

get '/api/stories' do
  content_type :json
  {stories: Story.all}.to_json
end

get '*' do
  send_file 'dist/index.html'
end

Note that this new route is "namespaced" under /api namespace, to avoid conflicting with the /stories route on the Ember side.

To get Ember to fetch data from the server instead of from the fixtures, just remove the fixtures, change the application adapter from a FixtureAdapter to the built in ActiveModelAdapter, and place the new adapter under the api namespace.

// app/adapters/application.js
import DS from 'ember-data';

export default DS.ActiveModelAdapter.extend({
 namespace: 'api'
});
// app/models/story.js
import DS from 'ember-data';

var Story = DS.Model.extend({
  title: DS.attr('string'),
  url: DS.attr('string'),
  story_content: DS.attr('string')
});

export default Story;

Now your stories will be loaded and displayed when you visit localhost:4567! (Don't forget to restart server.rb and re-run ember build)

Don't Like Goodbyes - What to work on next

Congratulations, you have a working Ember/Sinatra application that can fetch and display news stories from any RSS feed in the World! The finished code for this tutorial is also available on Github. But its not very feature rich, and it certainly won't be winning any design awards. Below are some ideas of where to go next in developing the app. Some may form the basis of future blog posts, and hopefully others will inspire readers to build their own amazing news reading tools.

Improve the interface

Our interface is about as bare bones as could be. We could start by adding a route and template to display individual stories at the very least, but adding some basic design touches or starting to style our page with Bootstrap would be a good goal.

Read and unread stories

Once you've read a story, it'd be nice to mark it as read and not see it again. The first step would be adding the necessary fields to the Stories table in our database, and the Story model in our Ember application. After that, Ember Data's ActiveModelAdapter will automatically generate a standard RESTful PUT request when we save a story model on the client side, and all we'll have to do is add a put "/stories" route to our Sinatra server.

Automatically fetch new stories

Having to run the rake fetch_stories command every time we want to get new stories from an RSS feed. You could just hard code a list of feed urls and fetch them each time, but a more robust solution would be to create a new "Feed" model and database table, using it to remember which feeds to update. You could then regularly schedule updates using cron or the excellent whenever gem.

Pagination

Once you get more than a few stories in your database, sending them all from the server and rendering each one in the DOM will waste resources, especially if you're only there to view the first few stories. Although Ember doesn't have any built in pagination functions, the ember-cli-pagination addon is worth investigating. I've also had luck with simply managing a page variable in the ApplicationController, and using the will_paginate gem to handle things on the server side.

Switching to Postgres and deploying to Heroku

We used sqlite for this tutorial since its easy to setup and use from Sinatra, but as the number of stories in our database grows we'll want to switch to a more performant SQL software. We're big fans of PostgreSQL here at Bendyworks, and switching to it from sqlite isn't particularly difficult once you have it installed and configured. Switching to Postgres gives us another unexpected benefit however; its the last hoop to jump through before we can easily deploy our application to Heroku using the Ember-CLI buildpack. Then you can read your own personalized news feed from any internet connected device! (although you may want to start thinking about authentication before adding features that update the database)

Take the news into your own hands

Although building a news reader with Ember-CLI has been a great way to learn more Ember and experiment with serving up the assets and API through Sinatra, my original motivation was at least in part to start building a news reader that won't get shut down or bought and integrated into an app I like less. Beyond that, I wanted the ability to score and sort stories by a user controlled 'interestingness' metric. I've also always wanted to build a news reader and call it Bottomless Soup Bowl in honor of this ig nobel prize. It's still pretty experimental, but if you'd like to see a demo of what the app could look like after some of the features discussed here are added, check out bsb.herokuapp.com. Any input, suggestions, or Pull Requests are also very welcome on the wstrinz/bsb Github repository.


Category: Development
Tags: Ember, Ruby