Todo list using a Sinatra REST API
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.
more
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: