Sinatra Todo app with Datamapper using Postgres and JSON

By | 01/10/2014

This blog post really continues on the previous one. In that post, we created a Todo app using Sinatra, Postgres but we returned ERB files. Essentially, our app returned some HTML files that contained immediately all the data in a proper Bootstrap format.

In this case, the server part and the client part are really linked to each other. This might not be what you always want. More and more, applications are divided to clearly split the server from the client. In such scenario’s, the server exposes typically a REST API while the client parses the JSON or XML code that is send back by the REST Server. In this post, we will create such a REST server that returns JSON style responses to a client. In next posts, we will then create clients using some Javascript frameworks or mobile applications.

Compared to the previous post, the only change really is that we have changed the routes.rb file in the ‘routes’ folder to respond with JSON rather than referring to the ERB files. The routes.rb file now looks like:

  get "/" do
    format_response(Todo.all, request.accept)
  end

  get "/todos" do
    format_response(Todo.all, request.accept)
  end

  get "/todos/:id" do
    todo ||= Todo.get(params[:id]) || halt(404)
    format_response(todo, request.accept)
  end

  post "/todos" do
    body = JSON.parse request.body.read
    todo = Todo.create(
      content:    body['content'],
      created_at: Time.now,
      updated_at: Time.now
  )
  status 201
  format_response(todo, request.accept)
  end
   

  put '/todos/:id' do
    body = JSON.parse request.body.read
    todo ||= Todo.get(params[:id]) || halt(404)
    halt 500 unless todo.update(
      content:      body['content'],
      updated_at:   Time.now,
      completed_at: body['done'] ?  Time.now : nil,
      done:         body['done'] ?  true : false
    )
    format_response(todo, request.accept)
  end
  
  delete '/api/movies/:id' do
    todo ||= Todo.get(params[:id]) || halt(404)
    halt 500 unless todo.destroy
  end 

The full application can be found here in case you want to see a completed example. Let’s go ahead an run our app.

wim@wim-mint ~/Sinatra_Todo_Postgres_Datamapper_structure_json $ rake migrate
 ~ (0.000721) PRAGMA table_info("todos")
 ~ (0.000015) SELECT sqlite_version(*)
 ~ (0.013478) DROP TABLE IF EXISTS "todos"
 ~ (0.000045) PRAGMA table_info("todos")
 ~ (0.004265) CREATE TABLE "todos" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "content" VARCHAR(255) NOT NULL, "done" BOOLEAN DEFAULT 'f' NOT NULL, "completed_at" TIMESTAMP, "created_at" TIMESTAMP, "updated_at" TIMESTAMP)
wim@wim-mint ~/Sinatra_Todo_Postgres_Datamapper_structure_json $ shotgun
== Shotgun/WEBrick on http://127.0.0.1:9393/
[2014-09-12 11:44:17] INFO  WEBrick 1.3.1
[2014-09-12 11:44:17] INFO  ruby 2.1.2 (2014-05-08) [x86_64-linux]
[2014-09-12 11:44:17] INFO  WEBrick::HTTPServer#start: pid=5161 port=9393

Going to http://localhost:9393/todos will show just some brackets. This means our app is working. Since there is no data yet in the database, it will return an empty resultset. You could also verify this with a REST client. To do so, we’ll be using the POSTMAN extension to Chrome browser, which is an excellent graphical tool in case you don’t want to use cURL (which is perfectly well suited to test our REST API if you’re more into the CLI mindset).

In the below screenshot, you can see we did a GET http://localhost:9393/todos and we got back again the []. The fact you can do this using a random REST client shows that we really made our server app independent from the client app.

Postmap_get

Let’s add some data to the database using the Postman REST client. The model we are using is in below snippet. So this means that all the data items will be added to the database. The ‘id’ will be created automatically, the content field is mandatory and the done file will be set to false by default, indicating a todo item is never completed already when it is inserted in the database the first time. The date fields are not required, but will be automatically created in our code later on.

class Todo
  include DataMapper::Resource  
  property :id,           Serial,	key: true, unique_index: true
  property :content,      String,	required: true, length: 1..255
  property :done,         Boolean,  :default => false, required: true
  property :completed_at, DateTime
  property :created_at,   DateTime
  property :updated_at,   DateTime
end

However, in below snippet from out routes.rb file, you see that the app expects some data like the content. Note that you don’t have to supply the dates (created_at and updated_at) since they will be automatically set to the current time in our code.

  post "/todos" do
    body = JSON.parse request.body.read
    todo = Todo.create(
      content:    body['content'],
      created_at: Time.now,
      updated_at: Time.now
  )
  status 201
  format_response(todo, request.accept)
  end
 

So we wanted to insert some todo items to our database, wasn’t it? See the below screenshot on how to achieve this:
Postman_post

Note that the server replies back with a “200 OK” message which means the item was inserted successfully. If you’re not convinced by the “200 OK” message from Postman, you could also see that this succeeded by doing a GET request to the /todos as shown in below example

postman_post_success
Noteworthy is that in the ‘routes.rb’ snippet above, we refer to the format_response helper method. This can be found in the ‘helpers/response_format.rb’ file which looks like:

require 'sinatra/base'

module Sinatra
  module ResponseFormat
    def format_response(data, accept)
      accept.each do |type|
        return data.to_xml  if type.downcase.eql? 'text/xml'
        return data.to_json if type.downcase.eql? 'application/json'
        return data.to_json
      end
    end   
  end
  helpers ResponseFormat
end

In the above snippet, the ‘format_response(Todo.all, request.accept)’ will provide all the todo items to the function and the format depends on the Accept header of the request. This is pretty cool. If your client application -for some reason- prefers to receive XML instead of JSON, it can be achieved easily.
postman_xml

This means that when we set the Accept header to ‘text/xml’ in the Rest client (can be Postman or your own client application), it will be caught by our format_response function where the accept parameter will be equal to ‘text/xml’, hence our app will return XML data via the data.to_xml return statement. Isnt’ this pretty neat?

I have made the code available on Github (Sinatra-Todo-app-with-Datamapper-using-Postgres-and-JSON) so you can have a look how it all ties together.