coding and cooking

Awesome RESTful API with Rails

Awesome RESTful API with Rails

If you are coding in Ruby with Rails, at some moment inexorably you will need to build a RESTful API, to respond some external HTTP requests. So, let's consider to create a JSON API and let's do this with elegance and the right way, i.e., testing!



For this post, I'm using:

So, basically, our task will be to send HTTP requests and test the response status code for each one and it's body contents; it needs to match our expectations.

The app I created for this post is in a repository on Github. It's a products managing example app.

What we want to test?

As said previously, we will focus in two things: the response body and status code of it. I think this is the most important stuff of a good API application test. Testing the body we testify if our application is sending the right content, if it's possible. What you mean with "if it's possible"? Why could not be? Well, there comes one of the main purposes of the status tests. To make a request, a client/application or whatever it be need to get authorized.

Response Body

The body should have the resources that we've requested and only this; may not contain private attributes. Mainly for POST and DELETE, we need to ensure the logic is completed as the expected.

HTTP status codes

This is very cool to do; return the authorization status. And, status can return another purposes too. See below some common HTTP status responses:

  • 200 -> OK - The request was a success
  • 201 -> Created - Request fulfilled and resulted in a new resource created
  • 204 -> No Content - The server successfully processed the request, but is not returning any content
  • 401 -> Unauthorized - Authentication fail ("you don't have the necessary permissions for the resource")
  • 403 -> Forbidden - The request was valid, but the server is refusing to respond it
  • 404 -> Not Found - The requested resource could not be found

For a more complete list of status codes, see this.

So, let's get started. If you are creating a new Rails application, after running rails new, put in your Gemfile the gems factory_girl_rails and rspec-rails. If not, just do the same thing. In both cases, after that,
   

bundle install

 

Versioning

Since we are doing an API, it's important to make a versionable app. That's because you may need to maintain, for example, two versions of the API at a transition between this versions, for example. If you don't do this, you probably will need to change the request URL. And this is not good at all. Think you would need to make your API less clear if you create a controller that do the same thing the previous version but, for not being an versioned app, you need to create another name for the same thing. Versioning the app you can give the same names and namespaces if it has at list a layer of a version module (practically, a namespace). It gives you consistency and continuity, without cutting off users that still needs to use an old version of the API at the time you need to improve and release new versions

So, to make our app versionable, we can give our app a module called v1. I prefer, even before this module, create a module called api; in case you are doing an API in an existent app, this is a good manner to split it.

To do that, firstly let's set our routes going to ** config/routes.rb ** and changes the Products resources to be included in the api and v1 namespaces, like this

namespace :api
  namespace :v1
    resources :products
  end
end



Now, let's change our controller - ** app/controllers/products_controller.rb **. We have two ways to do that.

module api
  module v1
    class ProductsController < ApplicationController
    end
  end
end



or

class API::V1::ProductsController < ApplicationController
end



Sincerely, I prefer the first one.

Now, just one more step. Let's create the folders that separates the modules. So, create, inside ** app/controllers *, the folder * api ** and so the folder ** v1 *. The new structure is now * app/controllers/api/v1/products_controller.rb **

Then, our new routes will be:

HTTP Method Path                 Action
GET         /api/v1/products     /api/v1/products#index
POST         /api/v1/products     /api/v1/products#create
GET         /api/v1/products/:id /api/v1/products#show
PUT         /api/v1/products/:id /api/v1/products#update
DELETE       /api/v1/products/:id /api/v1/products#destroy


Great! Now our API support multiple versions without great structural changes that could be a headache in the request.

Rendering Response

The two most popular formats to perform API responses are XML an JSON (JavaScript Object Notation). As said before, we will use JSON because it has a a bunch of advantages over XML. Maybe the main advantages is that JSON has a much more simple syntax, which makes easier to human reading and faster (both in transmission - due to smaller size - and parsing). And Rails has an excellent built in support for that. It's so easy that creating a response in JSON is ridiculous like this

render json: { name: "Me Myself" }


The Rspec request specs

To use a API is to do an integration. So, we can say that API testing is integration testing. Our tests are HTTP requests tests to URLs defined in the application (API). So, we will use Rack::Test to do our request tests, through Rspec. The Rspec request tests provide a wrapper around Rails' integration tests and drive behavior through the full stack, including the Rails routing convention with no need of stubbing.

First, let's create our factory:

FactoryGirl.define do
  factory :borussia_product, class: 'Product' do
    name "Borussia Dortmund T-Shirt"
    brand 'Puma'
    available true
  end

  factory :real_product, class: 'Product' do
    name "Real Madrid T-Shirt"
    brand 'Adidas'
    available true
  end

  factory :vasco_product, class: 'Product' do
    name "Vasco da Gama T-Shirt"
    brand 'Umbro'
    available false
  end
end



So, let's get started with our tests. Let's write our first test.

describe "GET /products" do
  context "when requested" do
    before do
      FactoryGirl.create :borussia_product
      FactoryGirl.create :real_product

      get "/api/v1/products", {}, { "Accept" => "application/json" }
    end

    it "returns the correct status" do
      expect(response.status).to eql 200
    end

    it "returns all the t-shirts" do
      body = JSON.parse(response.body)
      products_names = body.map { |product| product['name'] }

      expect(products_names).to match_array(["Borussia Dortmund T-Shirt", "Real Madrid T-Shirt"])
    end
  end
end



The action could be like this

def index
  render json: Product.all, status: 200
end



Notice that you can omit the status: 200 directive 'cause the 200 (success) status is the default.

Notice too the "Accept" => "application/json" part. Accept is a HTTP header that defines what content-type are acceptable for the response.

Let's see the create test now.

describe "POST /products" do
  context "when request a product creation with it's permitted fields" do
    before do
      product_params = {
        product: {
          name: "Liverpool T-Shirt",
          brand: "New Balance"
        }
      }

      request_headers = {
        'Accept' => 'application/json'
      }

      post '/api/v1/products', product_params, request_headers
    end

    it "should create the product" do
      expect(Product.last.name).to eql "Liverpool T-Shirt"
    end

    it "returns the correct status" do
      expect(response.status).to eql 201
    end
  end

  context "when request a product creation with NO permitted fields" do
    before do
      product_params = {
        product: {
          name: "Liverpool T-Shirt",
          brand: "New Balance",
          available: true
        }
      }

      request_headers = {
        'Accept' => 'application/json'
      }

      post "/api/v1/products", product_params, request_headers
    end

    it "should create the product nut NOT set prohibited parameters" do
      expect(Product.last.available).to be false
    end

    it "returns the correct status" do
      expect(response.status).to eql 201
    end
  end
end



And here goes the action:

def create
  @product = Product.new(product_params)

  if @product.save
    render json: @errors, status: :created
  else
    render json: @product.errors, status: :unprocessable_entity
  end
end



Now, the update:

describe "PUT /products/:id" do
  context "when request a product update with it's permitted fields" do
    before do
 product = FactoryGirl.create(:borussia_product)

 product_params = {
   product: {
 brand: 'Warrior'
   }
 }

 request_headers = {
   'Accept' => 'application/json'
 }

 put "/api/v1/products/#{ product.id }", product_params, request_headers
    end

    it "should update the product" do
 expect(Product.last.brand).to eql 'Warrior'
    end

    it "returns the correct status" do
 expect(response.status).to eql 204
    end
  end

  context "when request a product update with NO permitted fields" do
    before do
 product = FactoryGirl.create(:borussia_product)

 product_params = {
   product: {
 brand: 'Adidas',
 available: false
   }
 }

 request_headers = {
   'Accept' => 'application/json'
 }

 put "/api/v1/products/#{ product.id }", product_params, request_headers
    end

    it "should create the product nut NOT set prohibited parameters" do
 expect(Product.last.available).to be true
    end

    it "should update the product" do
 expect(Product.last.brand).to eql 'Adidas'
    end

    it "returns the correct status" do
 expect(response.status).to eql 204
    end
  end
end



And it's action:

def update
  if @product.update(product_params)
    render json: @errors, status: 204
  else
    render json: { message: "Errors occured when trying to update product" }, status: :unprocessable_entity
  end
end



And, last but definitely not least, the delete:

describe "DELETE /products/:id" do
  context "when request a with a valid ID" do
    before do
 product = FactoryGirl.create(:real_product)

 request_headers = {
   'Accept' => 'application/json'
 }

 delete "/api/v1/products/#{ product.id }", {}, request_headers
    end

    it "should return the correct code" do
 expect(response.status).to eql 204
    end

    it "should delete the product" do
 expect(Product.count).to eql 0
    end
  end
end



And the refered action:

def destroy
  @product.destroy

  render nothing: true, status: 204
end



That's it! You did it, young grasshopper! \o/

Some tips

Default response format to JSON

An improvement is to set the way we handle responses to JSON as default. In ** config/routes.rb ** put

namespace :api, defaults: { format: :json } do
  namespace :v1 do
    resources :products
  end
end



Doing this, when sending a request to

http://www.example.com/api/v1/products.json



we can omit the format, so the request could be

http://www.example.com/api/v1/products


Subdomain and duplications

When possible, could be a reasonable idea split your API domain to balance the traffic at the DNS level. We can permits our routes to be served only on a specific subdomain. For example, with our api example above, we could specify the api subdomain like this

namespace :api, constraints: { subdomain: :api } do
  namespace :v1 do
    resources :products
  end
end



But now we have a request like

http://www.api.example.com/api/v1/products



We can remove the ** api ** word duplication doing this

namespace :api, path: '', constraints: { subdomain: :api } do
  namespace :v1 do
    resources :products
  end
end



and the url now is

http://www.api.example.com/v1/products


Database 'cleaner'

If you get some problem with restart your test environment for each test (variables, DB records, and etc...) - this 'problems' can occur according with your way to test; your manner to use describe, context and it directives - and you don't want to use gems like Database Cleaner to resolve it, I recommend you to put in your ** spec/spec_helper.rb **

config.after :all do
  ActiveRecord::Base.subclasses.each(&:delete_all)
end



This code is executed after tests. The best part about this is that will only clear those tables that you have effectively touched (untouched Models will not be loaded and thus not appear in subclasses, also the reason why this doesn't work before tests). The only downside to this is that if you have a dirty database before running tests, it will not be cleaned. But I doubt that is a major issue, since the test database is usually not touched from outside tests (if you do this - populate tests DB outside tests, PLEASE TELL ME WHY...).