I am attending to a post-degree program and one of its courses is Server-Side Scripting. The professor gave us a project which students should create a project and my colleague and I created a Sinatra REST API for a todo list application.

The back end is a decent example of how to use Sinatra and Active Record to create simple APIs. In the repository’s readme, I show how to install it and use it. In this post, I detail what I coded:

Managing dependencies and configuring the database

First, I need to specify the dependencies for this project. Here are the gems:

ruby '2.2.2' source 'https://rubygems.org' gem 'sinatra' gem 'sinatra-cross_origin' gem 'json' gem 'activerecord' gem 'pg' gem 'sinatra-activerecord' gem 'rake' group :development do gem 'shotgun' end

In this project, I use shotgun to update my Sinatra app without restarting the server every time that I change my app.rb . The gem sinatra-cross_origin is needed to allow me perform requests externally. In addition, I use activerecord as ORM and sinatra-activerecord to extends Sinatra with extension methods and Rake tasks.

The file environment.rb specifies the database credentials:

configure :production , :development do set :show_exceptions , true db = URI . parse( ENV [ 'DATABASE_URL' ] || 'postgres://127.0.0.1/todo' ) ActiveRecord :: Base . establish_connection( adapter : db . scheme == 'postgres' ? 'postgresql' : db . scheme, host : db . host, username : db . user, password : db . password, database : db . path [ 1 ..- 1 ] , encoding : 'utf8' ) ActiveRecord :: Base . class_eval do def self . reset_autoincrement (options = {}) options [ :to ] ||= 1 case self . connection . adapter_name when 'MySQL' self . connection . execute "ALTER TABLE #{ self . table_name } AUTO_INCREMENT= #{ options [ :to ] } " when 'PostgreSQL' self . connection . execute "ALTER SEQUENCE #{ self . table_name } _id_seq RESTART WITH #{ options [ :to ] } ;" when 'SQLite' self . connection . execute "UPDATE sqlite_sequence SET seq= #{ options [ :to ] } WHERE name=' #{ self . table_name } ';" else end end end end

I created a reset_autoincrement method that will be used in my seeds file. I will talk about it soon. I also created a cors.rb file that enables Cross Domain Resource Sharing (CORS).

configure do set :allow_origin , :any set :allow_methods , [ :get , :post , :options , :delete , :put ] enable :cross_origin end options "*" do response . headers [ "Allow" ] = "HEAD,GET,PUT,POST,OPTIONS,DELETE" response . headers [ "Access-Control-Allow-Headers" ] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept" end

Last not the least, I need to create a Rakefile that will be used on my rake tasks:

require 'sinatra/activerecord/rake' namespace :db do task :load_config do require "./app" end end

Defining models

As you can see in ERD above, my todo list has two entities: List and Task .

class Task < ActiveRecord :: Base belongs_to :list validates :name , presence : true validates :list_id , presence : true end class List < ActiveRecord :: Base has_many :tasks , dependent : :destroy validates :name , presence : true validates :color , presence : true , format : /\A#?(?:[A-F0-9]{3}){1,2}\z/i end

I used the validates method to ensure some valid data. The attribute color, for example, must be a hexadecimal color. The next step is creating the migrations:

$ rake db:create_migration NAME=lists

This command creates a file inside db/migrate . This is my migration file:

class Lists < ActiveRecord :: Migration def up create_table :lists do | t | t . string :name t . string :color end end def down drop_table :lists end end

The same applies to Task:

$ rake db:create_migration NAME=tasks

class Tasks < ActiveRecord :: Migration def up create_table :tasks do | t | t . string :name t . references :list , foreign_key : true end end def down drop_table :tasks end end

After defining the table schema, it’s time to create the tables on the database:

$ rake db:migrate

Creating the API routes

Here is the initial version of my app.rb :

require 'sinatra' require 'sinatra/cross_origin' require 'sinatra/activerecord' require './config/environments' require './config/cors' require './models/list' require './models/task' require 'json' before do content_type :json end get '/lists' do List . all . to_json( include : :tasks ) end get '/lists/:id' do List . where( id : params [ 'id' ] ) . first . to_json( include : :tasks ) end post '/lists' do list = List . new(params) if list . save list . to_json( include : :tasks ) else halt 422 , list . errors . full_messages . to_json end end put '/lists/:id' do list = List . where( id : params [ 'id' ] ) . first if list list . name = params [ 'name' ] if params . has_key?( 'name' ) list . color = params [ 'color' ] if params . has_key?( 'color' ) if list . save list . to_json else halt 422 , list . errors . full_messages . to_json end end end delete '/lists/:id' do list = List . where( id : params [ 'id' ] ) if list . destroy_all { success : "ok" } . to_json else halt 500 end end

The routes for Task entity are very similar:

get '/tasks' do Task . all . to_json end get '/tasks/:id' do Task . where( id : params [ 'id' ] ) . first . to_json end post '/tasks' do task = Task . new(params) if task . save task . to_json else halt 422 , task . errors . full_messages . to_json end end put '/tasks/:id' do task = Task . where( id : params [ 'id' ] ) . first if task task . name = params [ 'name' ] if params . has_key?( 'name' ) if task . save task . to_json else halt 422 , task . errors . full_messages . to_json end end end delete '/tasks/:id' do task = Task . where( id : params [ 'id' ] ) if task . destroy_all { success : "ok" } . to_json else halt 500 end end

I added two extra routes to this application. One will render a index.html file, which I describe the project. The another one will populate the database with my initial data:

get '/' do content_type :html send_file './public/index.html' end get '/refresh' do # Clean the database and create the initial data load './db/seeds.rb' end

Time to create the config.ru file, responsable for start the application:

require './app' run Sinatra :: Application

Everything is done, let’s go:

$ shotgun

Hopefully, shotgun should return something like:

== Shotgun/WEBrick on http://127.0.0.1:9393/ [2016-08-03 19:51:50] INFO WEBrick 1.3.1 [2016-08-03 19:51:50] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14] [2016-08-03 19:51:50] INFO WEBrick::HTTPServer#start: pid=13348 port=9393

After that, you will be able to do requests as I did:

Seeding initial data

The file db/seeds.rb populates the database with initial data:

Task . delete_all List . delete_all Task . reset_autoincrement List . reset_autoincrement puts 'Creating sample lists' colors = [ '54b2a1' , '95d5cf' , '809bbe' , '98d2f3' , '80bf86' , 'a3d49f' ] [ 'Todo' , 'Movies' , 'Supermarket' ]. each_with_index do | list, index | List . find_or_create_by( name : list, color : colors [ index ] ) end puts 'Creating sample tasks' [ 'Nathan\'s Assignment' , 'Go to Meetup' ]. each do | task | Task . find_or_create_by( name : task, list : List . where( name : 'Todo' ) . first) end [ 'The Godfather' , 'Star Wars' ]. each do | task | Task . find_or_create_by( name : task, list : List . where( name : 'Movies' ) . first) end [ 'Milk' , 'Bread' , 'Butter' ]. each do | task | Task . find_or_create_by( name : task, list : List . where( name : 'Supermarket' ) . first) end

Deployment – Heroku

In order to deploy the app on Heroku, create a Procfile :

web: bundle exec ruby app.rb -p $PORT

Another option is clicking on the “Deploy to Heroku” button on the repository page. Heroku will look for the app.json file and setup all that you need. If you area curious about this file:

{ "name" : "todo-api" , "description" : "A simple Sinatra REST API" , "keywords" : [ "sinatra" , "api" , "activerecord" , "reminders" ], "repository" : "https://github.com/leonardofaria/todo-api" , "addons" : [ "heroku-postgresql:hobby-dev" ], "env" : { "RACK_ENV" : "production" }, "scripts" : { "postdeploy" : "bundle exec rake db:migrate && bundle exec rake db:seed" } }

This is not the focus of this post, then read the Heroku documentation [1, 2] if you are interested on this. Pretty interesting.

Final thoughts

I am not sure if I described everything but I hope that you got the main idea. You can take a look in the source code on Github, fork it or star it.

A web app (gif above) was also created to show how to use the API. The web app is also available in Github.

Other references

Part of my work was inspired in the following articles: