How to simply create JSON REST services using ActiveRecord and Sinatra

I spent the past two years+ (or so...) learning and experimenting a lot with mobile devices, making demo apps and such.

But we all know that apps aren't just the thing you install on your phone, there is also back-ends, APIs, services. For this I needed a very quick way to accomplish this without have too much time to put on it.

First, I am not a Rails developer by any means but i know the fundamentals of Ruby and with the help of Sinatra (a micro-framework that I personally love) and ActiveRecord on top of that is the perfect combo to get it done QUICKLY!.

It's necessary that you know Ruby programming basics to follow this example.

Table of Contents

Installing Ruby using RVM (Ruby Version Manager)

I choose RVM to work with ruby on my dev environment (and sometimes even on production, when using some kind of shared servers). Its a way to keep everything under control and don't mess up with my computer's/server packages when trying to work with different versions of ruby, gems, etc.

First we install RVM, then we use it to install ruby.... and finally the gem "bundler" that we are going to use to manage our application's dependencies.

# Installing RVM
curl -sSL https://rvm.io/mpapis.asc | gpg --import -
curl -L https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm

# Installing Ruby
rvm install 2.2.2
rvm --default use 2.2.2

# Install the bundler Gem
gem install bundler

Installing all necessary Gems

First we create the Gemfile file (yes, a file without extension); where we list all the gems/libraries that we are going to use in our application.

# Gemfile

source 'https://rubygems.org'

# SinatraRB Framework
gem "sinatra"

# Our database gem/mapper/model
gem "activerecord"
gem "sinatra-activerecord"

gem "json"

# SQLite
gem "sqlite3"

# With this we are going to require tux only on development mode.
# tux is a tool that allows you to interact with your Sinatra app from a command line
# We are going to use that to insert/fill our DB and test a few things.
group :development do
 gem "tux"
end

Now we just need to run a simple command and let bundler do the hard work for us. It may take a while to finish.

bundle install

Writing the base Application

Thanks to Sinatra this is very simple and we only need to create a few files. The code is pretty self-explanatory:

Rakefile

# Rakefile

require './application'

require 'sinatra/activerecord/rake'

config.ru

# config.ru

require 'sinatra'

set :run, false
set :environment, ENV['RACK_ENV'] || 'development'

require './application'
run Sinatra::Application

models.rb

# models.rb

# This file is empty for now.... going to work on it later

application.rb

# application.rb
require 'sinatra'
require 'sinatra/activerecord'

# Our file with our model Classes
require './models'

configure do
  # Config our Database, in our case is a simple file-based SQLITE

  ActiveRecord::Base.establish_connection(
    :adapter => "sqlite3",
    :database => "development.db"
  )

  set :show_exceptions, true

  # Un-comment this if you are using Vagrant or any type of VM/container such as Docker
  # set :bind, '0.0.0.0'
end

################################
# Application Routes
################################

# Home
get "/" do
  "Hello, this is our application homepage. But we aren't going to do anything with it."
end

# Service1: Return all our Text entities
get "/api/texts/all" do
  
   # We are going to implement our service here later on....

end

# Service2: Return one Text entity, by ID
get "/api/texts/:id" do
  
  # We are going to implement our service here later on....

end

################################
# 404
################################
not_found do
  'Whatever you are looking for its not here!'
end

################################
# 500
################################
error do
  'Sorry there was a nasty error - ' + env['sinatra.error'].name
end

Now to test our application.

ruby application.rb

That will tell your what URL to open on your web browser.

Creating / Working with the Database

We are going to create a migration; if you are not familiar with these a migration is some sort of file where you write a couple instructions that in our case will create our Tables in our database. First we run:

rake db:create_migration NAME=rest_entities

This will create a file like: db/migrate/20170820014906_rest_entities.rb. (After running that command the path to edit is returned), so let's just do that:

# db/migrate/20170820014906_rest_entities.rb

class RestEntities < ActiveRecord::Migration[5.1]
  def change
    create_table :texts do |t|
      t.string :name, :null => false
      t.string :message

      t.timestamps
    end
  end
end

Then to apply our changes to the DB:

rake db:migrate

Creating the Model

Now lets add some content to our models.rb file that we created previously; here we define the class to map our table content.

#models.rb
class Text < ActiveRecord::Base
  # Some logic could be here, but this is a simple example
end

Filling the database using (Tux)

Let's fill the database with a some data, for that we are going to use the tux gem... this opens a new shell to interact with our application:

vagrant@vagrant-ubuntu-trusty-64:/vagrant$ tux
>> text1 = Text.new
>> text1.name = "Jon"
>> text1.message = "Whats up?"
>> text1.save

>> text1 = Text.new
>> text1.name = "Annie"
>> text1.message = "Not much, swiping left"
>> text1.save

Before exit tux you can verify all the entries were created by simply:

>> Text.all

Type exit to close the shell.

Creating the JSON Services

Finally lets edit our application.rb file and add a few lines.

The magic happens when we use to_json, a very powerful helper that helps you serialize all kind of ActiveRecord objects. Check the examples bellow and also the next point where I explain a few extra options available.

# application.rb

# ......

# Service1: Return all our Text entities
get "/api/texts/all" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # Text.all.order returns all entities in the database
  # 
  # to_json: 
  # It serializes the results into a JSON objects, in this
  # case its an array of elements. Each element contains all the
  # fields in the table: id,name,message,created_at,updated_at
  #
  Text.all.order(created_at: :desc).to_json
end

# Service2: Return ONLY ONE Text entity, by ID
get "/api/texts/:id" do
  # Lets get one Text element from our DB by id
  text = Text.find_by_id params[:id]
  if text.nil?
    halt 404
  end

  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # Now we return the serialized object.
  #
  # By using the :only option, we can especify what fields we want to return
  #
  text.to_json({:only => [:name, :message]})
end


# ......

More JSON options / reference

to_json

This returns a JSON string representing the model. This can be returned as the HTTP response. 

as_json

Returns a hash representing the model.  This is useful when you want to build the response yourself, manually:

get "/api/texts/custom" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # Build our own object, just as example return two times the same query
  data = {
    :query1 => Text.all.as_json,
    :query2 => Text.all.as_json,
    :status => "OK"
  }

  # Now lets just return object response
  data.to_json
end

Options

:only/ :except

This is an array of what fields you want to include (only), or the ones you don't.

get "/api/texts/test1" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # Returns only the id and name fields.
  Text.all.to_json(:only => [:id, :name])
end

get "/api/texts/test2" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # Returns all the fields, except created and updated dates
  Text.all.to_json(:except => [:created_at, :updated_at])
end

:methods

Here you can specify one or more methods of the ActiveRecord model to be added as a field to the JSON, the value is whatever the method returns.

#models.rb

class Text < ActiveRecord::Base
  # our custom method
  def my_method
    "#{self.name} says: #{self.message}"
  end
end
# application.rb

# .......

get "/api/texts/custom" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  Text.all.to_json({:methods => [:my_method]})
end

# .......

:include

This is how we can include entity associations as a field. Also, you can "nest" options for them.

# application.rb

# .......

get "/api/texts/custom" do
  # Sets the response content type
  # Header: Content-Type: application/json
  content_type :json

  # otherentity represents the associated entity (this isnt implemented in the example)
  Text.all.to_json({:include => { :otherentity => {:only => [:id, :name]}}})
end

# .......

Refactoring the Model

If you have several services you can implement as_json in the model itself to avoid some code duplication:

#models.rb 
class Text < ActiveRecord::Base
  def as_json(options)
    super({:only => [:name, :message]}.merge(options))
  end
end

Download the project

I uploaded the working project directly to a Github so you can just copy-paste it without reading the article :)

Download the code here

About
Related Posts
Comments
The comments are closed on this section.
Hire me!

I'm currently open for new projects or join pretty much any project i may fit in, let me know!

Read about what I do, or contact me for details.

Categories