View on GitHub

JSON API with Ruby on Rails

Test Driven Development of a Versioned, RESTful API


Appropriate for use with RESTful clients such as Ember-Data & RestKit


Using

ActiveModelSerializers - Serialization in line with the jsonapi.org spec
MiniTest & Rack::Test - API Integration tests
FactoryGirl - Factory Generation for tests

Source at https://github.com/robj/widgetapi

Application Setup

Create a new rails application.

$ rails new widgetapi

Add the active_model_serializers gem and run bundler.

#Gemfile
gem 'active_model_serializers

$ bundle

Model

Generate a Widget model and migrate the DB

$ rails g model widget name:string supplier:string cost:integer
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

API Versioning

Ensure Rails autoloads /lib/*

#config/application.rb


module Widgetapi
  class Application < Rails::Application


     config.autoload_paths += %W(#{config.root}/lib)
     ...


Add ApiConstraints for header based version control.
(see http://jes.al/2013/10/architecting-restful-rails-4-api)
If no version is explicitly requested, fall back to V1.

#lib/api_constraints.rb

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.myapp.v#{@version}")
  end
end

Routing

Create a widget resources route under the API namespace.

#config/routes.rb


namespace :api, defaults: {format: :json} do

  scope module: :v1, constraints: ApiConstraints.new(version: 1, default: :true) do
      resources :widgets
  end

end

Serializer


rails g serializer widget

Add appropriate fields to the widget serializer.

#app/serializers/widget_serializer.rb

class WidgetSerializer < ActiveModel::Serializer
  attributes :id, :name, :supplier, :cost
end


Controller


#app/controllers/api/v1/widgets_controller.rb 

class Api::V1::WidgetsController < ApplicationController
end


MiniTest Setup

Add appropriate Gems to the Test environment and run bundler.

#Gemfile

group :development, :test do

  gem 'minitest'
  gem "rack-test", require: "rack/test"
  gem 'database_cleaner'
  gem 'factory_girl'
  gem 'random-word'


end

$ bundle

Setup a helper for minitest.

#test/minitest_helper.rb


ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'minitest/autorun'
require "active_support/testing/setup_and_teardown"
require 'factory_girl'
require 'database_cleaner'
require "rack/test"

FactoryGirl.find_definitions

DatabaseCleaner.strategy = :truncation
DatabaseCleaner.start


class MiniTest::Spec
 
  include Rails.application.routes.url_helpers
  include Rack::Test::Methods
  
  before :each do
    DatabaseCleaner.clean
  end
 
  after :each do

  end

  def last_response_json
    JSON.parse(last_response.body)
  end
 
end

Integration Test

Create a skeleton test for the Widget API.

#test/integration/widget_api_integration_test.rb

require "minitest_helper"

def app
    Rails.application
end


describe "API Widget integration" do

  it does "nothing" do
  end

end


Ensure our test environment works correctly with a blank test.

$ ruby -Itest test/integration/widget_api_integration_test.rb 

Factory

Create a Widget factory

#test/factories/widget.rb

FactoryGirl.define do

  factory :widget do
    name {"#{RandomWord.adjs.next}"}
    supplier {"#{RandomWord.adjs.next} Supplier"}
    cost {(1..99).to_a.sample}
  end


end

Index action

Controller

class Api::V1::WidgetsController < ApplicationController


    def index
      widgets = Widget.all
      render json: widgets
    end

    ...



Integration Test

    it "should list widgets index" do 

                widget_creation_count = 22

                (1..widget_creation_count).each do 
                    FactoryGirl.create(:widget)
                end

                get '/api/widgets'
                assert last_response.successful?
                assert last_response_json['widgets'].count.must_equal widget_creation_count


    end


Show action

Controller

   def show

        widget = Widget.find(params[:id])

        if widget
          render json: widget
        else
          head :not_found
        end

    end

Integration Test

    it "should get a widget with expected keys" do

        widget =  FactoryGirl.create(:widget)

        get "/api/widgets/#{widget.id}"
        assert last_response.successful?
        assert last_response_json['widget'].has_key?('id').must_equal true
        assert last_response_json['widget'].has_key?('name').must_equal true
        assert last_response_json['widget'].has_key?('supplier').must_equal true
        assert last_response_json['widget'].has_key?('cost').must_equal true

    end

Create action

Controller
- Uses strong_params to prevent mass assignment attacks.

    def create

        widget = Widget.new(widgets_params_create)

        if widget.save
            render json: widget
        else
            head :internal_server_error
        end

    end



private


  def widgets_params_create
    permitable_params = [:name, :supplier, :cost]
    params.require(:widget).permit(permitable_params)
  end

  def widgets_params_update
    permitable_params = [:supplier, :cost]
    params.require(:widget).permit(permitable_params)
  end

Integration Test

    it "should create a new widget" do

        widget = { name: 'widgee', supplier: 'widgsoft', cost: 32 }

        post '/api/widgets', {widget: widget}
        assert last_response.successful?
        assert last_response_json['widget']['name'].must_equal 'widgee'

    end

Update action

Controller

    def update

        widget = Widget.find(params[:id])

        if widget.update_attributes(widgets_params_update)
            render json: widget
        else
            head :internal_server_error
        end


    end

Integration Test

    it "should get and update a widget" do

        widget = FactoryGirl.create(:widget)

        get '/api/widgets'
        assert last_response.successful?
        widget_json =  last_response_json['widgets'].first

        widget_id = widget_json['id']
        widget_json['supplier'] = 'supplierX'

        put "/api/widgets/#{widget_id}", {widget: widget_json}
        assert last_response.successful?
        assert last_response_json['widget']['supplier'].must_equal 'supplierX'

    end



    it "should get and update a widget, ensuring strong_params prevents name being updated" do

        widget = FactoryGirl.create(:widget)

        get '/api/widgets'
        assert last_response.successful?
        widget_json =  last_response_json['widgets'].first

        widget_id = widget_json['id']
        widget_original_name =  widget_json['name'] 
       
        widget_json['name'] = 'nameX'
        put "/api/widgets/#{widget_id}", {widget: widget_json}

        assert last_response.successful?
        assert last_response_json['widget']['name'].must_equal widget_original_name

    end


Destroy action

Controller
- Demonstrates use of auth_token to prevent unauthorized deletion.

    def destroy

        return head :unauthorized unless (params.has_key?('auth_token') && params[:auth_token] == 's3cr3t')

        widget = Widget.find(params[:id])

        if widget.delete
            head :no_content
        else
            head :internal_server_error
        end


    end

Integration Test

    it "should get and delete a widget if authorized" do

        auth_token = 's3cr3t'
        widget = FactoryGirl.create(:widget)

        get '/api/widgets'
        assert last_response.successful?
        widget_json =  last_response_json['widgets'].first
        widget_id = widget_json['id']

        delete "/api/widgets/#{widget_id}", {auth_token: 'incorrect'}
        assert last_response.client_error?
        assert last_response.status.must_equal 401 #unauthorized
 
        delete "/api/widgets/#{widget_id}", {auth_token: auth_token}
        assert last_response.successful?
        assert last_response.status.must_equal 204 #no_content

    end