giovedì 24 luglio 2014

Build a simple RESTful Web Service JSON API with Ruby and Sinatra

In one my activities I have the needs to quickly prototype a RESTful web service API.
Obviously I want to do it with my current preferred language Ruby.

It is clear I can easily do it with Rails but probably it will be an overkill for what I need to do.
Reading different articles about major differences between Rails and Sinatra micro-framework I have decided to build the simple REST web service I need using Sinatra.

I am here sharing what I have done. This is something I put together quite quickly, so it is not complete.
Anyone of you which wants to help can contribute.

Let's install the necessary ruby gems.

# cd ~
#
# mkdir rails_api
#
# mkdir rails_api/json
#
# sudo gem install sinatra
#
# sudo gem install rabl
#
# sudo gem install oj
#
# sudo gem install data_mapper
#
# sudo gem install dm-sqlite-adapter
#
# sudo gem install thin

I am using Rabl gem as this provides great flexibility in creating JSON replies to API request

Now in the rails_api/json directory create 3 files with the below content

#post.rabl
object @post
attributes :id, :title, :body

#posts.rabl
collection @posts
attributes :id, :title, :body

#error.rabl
object @error
attributes :message

Those files render the JSON replay to our RESTful API call. Now in the rails_api directory create the following file
#rest_api.rb

require 'rubygems'
require 'sinatra'
require 'rabl'
require 'data_mapper'
require 'json' 

######## setup sqlite DB ########
DataMapper.setup(:default, 'sqlite:rest_api.db')

######## Post Model ########
class Post
  include DataMapper::Resource

  property :id,         Serial    # An auto-increment integer key
  property :title,      String    # A varchar type string, for short strings
  property :body,       Text      # A text block, for longer string data.
end

DataMapper.finalize
Post.auto_upgrade!


######## Very simple Error class ########
class Error 
 attr_accessor :message
 def initialize(mex)
  @message = mex
 end
end

######## register Rabl. This is required for Sinatra ########
Rabl.register!

######## retrun 404 in case of invalid URL request ########
not_found do
 status 404 #page not found
end

######## simple helper to return HTTP 400 ########
def status_400(mex)
 @error = Error.new(mex)
 status 400
 rabl :'error', :format => "json", :views => 'json' 
end

######## simple helper to return HTTP 404 ########
def status_404(mex)
 @error = Error.new(mex)
 status 404
 rabl :'error', :format => "json", :views => 'json' 
end

######## simple helper to return HTTP 200 ########
def status_200
 status 200
end

######## simple helper to return HTTP 201 ########
def status_201(link)
 status 201
 #headers "location:/post/43"
 headers "Location"   => link
end

######## REST CRUD APIs for Post resource ########

######## index of Post ########
#GET post
get '/post' do 
 @posts = Post.all
 if @posts.empty?
  status_404("The reuqested resource does not exists")
 else
  status_200
    rabl :'posts', :format => "json", :views => 'json'
   end
end

######## show one Post ########
# GET /post/:id
get '/post/:id' do
 @post = Post.get(params[:id])
 if @post.nil?
  status_404("The reuqested resource does not exists")
 else
  status_200
    rabl :'post', :format => "json", :views => 'json'
   end
end

######## create a Post ########
#POST /post
post '/post' do
 begin
  post_data =  JSON.parse request.body.read 

  if post_data.nil? or !post_data.has_key?('title') or !post_data.has_key?('body')
   raise "exception"
  else
   post = Post.create(:title => post_data['title'], :body => post_data['body'])
   post.save
   status_201("/post/#{post.id}")
  end
 rescue => ex
  status_400("Invalid client request")
 end
end

######## Update a Post ########
#PUT /post/:id
put '/post/:id' do
 begin
  put_data =  JSON.parse request.body.read 

  if put_data.nil? or !put_data.has_key?('title') or !put_data.has_key?('body')
   raise "exception"
  else
   post = Post.get(params[:id])
   if post.nil?
    status_404("The reuqested resource does not exists")
   else
    post.title = put_data['title']
    post.body = put_data['body']
    post.save
    status_200
   end
  end
 rescue => ex
  status_400("Invalid client request")
 end

end

######## Remove a Post ########
#DELETE /post/:id
delete '/post/:id' do
 post = Post.get(params[:id])
 if post.nil?
  status_404("The reuqested resource does not exists")
 else
  post.destroy
  status_200
 end
end
Ok now just run it with
# ruby rest_api.rb -o 0.0.0.0
You should get something like this
== Sinatra/1.4.5 has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.6.2 codename Doc Brown)
Maximum connections set to 1024
Listening on 0.0.0.0:4567, CTRL+C to stop

Let's now test our RESTful Web Service. You can use your preferred tool like POSTMAN in Chrome.
I will use the always available curl to test the typical CRUD functions

CREATE

# curl -H "Content-type: application/json" -X POST  -d '{"title": "post1", "body": "this is post 1"}'  http://127.0.0.1:4567/post
READ
# curl  http://127.0.0.1:4567/post

# curl  http://127.0.0.1:4567/post/1
UPDATE
# curl -H "Content-type: application/json" -X PUT  -d '{"title": "post 5", "body": "this is post #5"}'  http://127.0.0.1:4567/post/1
DELETE
# curl -X DELETE http://127.0.0.1:4567/post/1

I am sure it is quite easy to argue that the HTTP return code I have used are not correct. Looks like this is quite an open topic.

In my example here I have mapped HTTP Status Code to REST call according to what described in this article (Using HTTP Status Codes correctly in your REST Web API)

As always let me know about any improvement, suggestion or correction.

Hope this helps someone out there.