sorry

I’m Totally Sorry

Oops, seems I haven’t actually followed through on my commitment to write a blog post every month, shame on me!

Down to Bizness

Lets start off by creating something difficult. An application that is incredibly useful, original, cutting-edge and technically challenging to develop. What else could it be but a TODO list of course.

bizcat From scratch we are going develop a TODO list API with Sinatra. If you are unfamiliar with Sinatra it is simply a small web framework for Ruby, somewhat like Ruby on Rails. As Sinatra is built on top of Rack we can then easily deploy our completed app to Heroku.

This tutorial expects that you have some grounding in Ruby and understand basic RESTful API design. We will be using Ruby 2.1.2 but any Ruby version post-2.0 should be fine.

Setting up your dependencies

Even in a small project nobody likes managing their own dependencies, it really can be terrible so lets reach for Bundler, which essentially frees you from dependency hell by making sure your application runs the same code on every machine.

First we need to install the Bundler gem.

$ gem install bundler

We then must define our according Gemfile, a Ruby file placed at the root of your directory for describing your gem (package) dependencies.

source 'https://rubygems.org'
# ruby '2.1.2' # tested with 2.1.2 but other versions should be fine

gem 'sinatra', '1.4.2' # web framework
gem 'sinatra-contrib', '1.4.2' # web framework extensions

gem 'data_mapper' # lightweight ORM

group :development do
  gem 'dm-sqlite-adapter' # local DB ORM adapter
  gem 'sqlite3' # local DB
end

group :production do
 gem 'pg' # prod DB
 gem 'dm-postgres-adapter' # prod DB ORM adapter
 gem 'unicorn' # prod application server
end

Finally we must install our required gems with Bundler.

$ bundle install --without production

Say Harro World!

Great, we have our dependencies in place so we better verify that everything is working as expected. Any Ruby file which requires or loads the Sinatra library, with at least one endpoint defined, can be conveniently executed as a WEBrick server.

Lets simply define one route or endpoint and test it out.

require 'sinatra'

get '/' do
  "LOLOLOL! You rock!"
end

Like any other Ruby script, we can execute it with the ruby command.

$ ruby proof.rb

As you would expect proof.rb begins to run on a default port and we see the desired response on http://localhost:4567/. Among other things, we can easily change this target port number by passing a flag argument to the script.

A list of the available flags can be outputted by passing -h as an argument.

App Environment Cruft

Lets now create our cleverly named main application file, app.rb. We need to then first require our library modules.

# Require the bundler gem and then call Bundler.require to load in all gems
# listed in Gemfile.
require 'bundler'
Bundler.require(:default, :development) if defined?(Bundler)
# Reload app.js when changed
require "sinatra/reloader" if development?

Using Bundler.require we tell Bundler to load those modules in the gem groups named development and default, where the latter is all of those gems not contained in the development or production groups.

Remember our dependency sinatra-contrib? It just contains lots of nice extensions for Sinatra. One of which no one should live without is Sinatra::Reloader. It automatically reloads all files required by your application which of course we need during development.

Models, Models Everywhere

We are using DataMapper as our ORM. In terms of our requirements its main appeal is the ever useful auto_migrate! feature which creates your datastore schema based off your defined DataMapper::Resource models, removing the need to write and manage migrations.

Now we must create our local SQLite DB and establish a connection.

# DB init
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/development.db") if development?

With such in place we can now define our naive Task which models a TODO list item. Similar to ActiveRecord, in DataMapper the convention with model names is to use the singular version of the name. Though unlike ActiveRecord, DataMapper does not enforce this.

# Models
class Task
  include DataMapper::Resource

  property :id, Serial, key: true
  property :name, String, required: true
  property :completed_at, DateTime
  property :created_at, DateTime
end
# Finalize and migrate
DataMapper.finalize.auto_upgrade!

Before using our model we must first finalize. This checks the validity of all of our DataMapper model definitions and initializes all properties associated with relationships. This nicely returns its caller, DataMapper, so we can chain a call to auto_upgrade! which issues the necessary CREATE statement for our task table if it does not already exist.

Now lets verify that our model is in fact persistent and working as expected. We can simply execute script and our DB will be in place. Lets query the current amount of task rows.

$ ruby -r ./app.rb -e "puts Task.all.size"

Gimme Them Endpoints

In Sinatra we can define endpoints that are a combination of a HTTP verb and the according request path. Which as you can see results in succinct and pretty code ♥

# Api
get '/task/' do
  content_type :json

  @tasks = Task.all(order: :created_at.desc)
  @tasks.to_json
end

Upon a defined endpoint being accessed, a before code block is ran if defined. This is a simple before-request hook, we do not avail of it for our endpoints. Upon its successful termination, the user-defined block on that endpoint will be executed.

Above we simply return a JSON list of all the task rows. We can verify this endpoint by simply executing curl http://localhost:4567/task/. As expected the body of our response is an empty array.

Now we can define our remaining endpoints for POST, GET,PUT and DELETE.

post '/task/' do
  content_type :json

  @task = Task.new(permit_params)
  @task.save ? @task.to_json : halt(500)
end

put '/task/:id' do
  content_type :json

  @task = Task.get(params[:id].to_i)
  @task.update(permit_params)

  @task && @task.save ? @task.to_json : halt(500)
end

get '/task/:id' do
  content_type :json

  @task = Task.get(params[:id].to_i)
  @task ? @task.to_json : halt(404)
end

delete '/task/:id' do
  content_type :json

  @task = Task.get(params[:id].to_i)
  @task && @task.destroy ? { success: "ok" }.to_json : halt(400)
end

def permit_params
  params.select { |k, v| ["name", "completed_at"].include? k }
end

Data, Not Enough Data

Before we deploy to Heroku it would be nice to have a prepopulated API in place, so lets just seed two tasks.

# Seed
if Task.count == 0
  Task.create(name: "Test Task One")
  Task.create(name: "Test Task Two")
end

Again, we can verify this by executing curl http://localhost:4567/task/ and see our listed tasks.

Ship First, Test Later

Lets put this thing of beauty out there! The usual tweaks are required for our Heroku deploy. We need to parameterize what gems are required and what DataMapper connection we wish to use per environment.

Bundler.require(:default, ENV['RACK_ENV'] || :development) if defined?(Bundler)

if development?
  DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")
elsif production?
  DataMapper.setup(:default, ENV['DATABASE_URL'] || 'postgres://localhost/sinatra-todos')
end

We then need to create a rakeup file named config.ru to instruct Heroku how to run our Rack app.

require './app'
run Sinatra::Application

Finally, we must setup a repository, add a Heroku remote and push our work to deploy the app.

git init && git add --all && git commit -m "initial"
heroku create
git push heroku master