Hawaiʻi's Technology Community

writing a json api service in Rails

In the past few years, I've written a few json API's in rails.  There are a few trends driving the popularity of API's, one being Service Oriented Architecture, and another being client side rendering.  

Service Oriented Architecture can apply to all services, not just browser accessible ones.  It consists of making services available via a standard like SOAP or JSON, to increase re-usability and modularity, SOAP and JSON being the two most popular SOA protocols.  SOAP is XML-based, while JSON is Javascript object based, and therefore consists simply of arrays, hashes, and primitive types.  JSON is by far the more popular approach in the web and Ruby communities, and is a more lightweight approach than SOAP.  JSON is also the most popular choice for browser facing API's.  Browser facing API's are gaining a lot of momentum as rich clients gain popularity and more of the rendering responsibility is being offloaded to the client for reasons of speed, interactivity, and integration with client-side MVC frameworks.

A simple JSON API in Rails might look like this:

 1|class Api::V1::SelfCare::ReceiptsController < ApplicationController

 2 | respond_to :json

 3|  def payment_posted
 4|    [:contract_id, :amount, :payment_type].each do |field|
 5|      unless params[field]
 6|        return respond_with({:success => false, :reason => "missing field: #{field}"}, :location => nil, :status => 400)
 7|      end
 8|    end
 9|    contract_id = params[:contract_id
10|   amount = params[:amount]
11|   payment_type = params[:payment_type]
12|   begin
13|      Float(amount)
14|    rescue ArgumentError => e
15|      return respond_with({:success => false, :reason => "invalid amount: #{amount}"}, :location => nil, :status => 400)
16|    end
17|    unless ['CREDIT', 'DEBIT'].include?(payment_type)
18|      return respond_with({:success => false,
19|                                              :reason => "invalid payment_type: #{payment_type}"},
20|                                              :location => nil, :status => 400)
21|    end
22|    user = User.find_by_contract_id(contract_id)
23|    if user
24|        PaymentMailer.payment_posted(user, amount, payment_type).deliver
25|        respond_with({:success => true}, :location => nil)
26|    else
27|      respond_with({:success => false, :reason => 'invalid contract_id'}, :location => nil, :status => 404)
28|    end
29|  end

Line 2 is a Rails directive to set this controller to respond to JSON type requests.  This service only has one endpoint, which corresponds to the payment_posted method on line 3.  In lines 4-8, I check that all required parameters are specified.  In case of one or more missing values, I return a JSON response indicating the first missing value. I also return an HTTP status of 400.  The community seems to be divided on whether JSON API's should return status as status codes or JSON messages, so I return both.  The status code is useful for client side frameworks that make use of such status codes, like jQuery.  The JSON message can be useful for debugging or manual testing, and can be used to return status information of arbitrary structure that an HTTP status code can't communicate.

Lines 12-16 make sure the amount parameter is parseable as a Float type, and return a message otherwise.  Notice that in Rails controllers, rendering does not return from the controller method. It's actually necessary to specify the return keyword to avoid double rendering errors to return from a method before the control flow does.

Lines 17-21 check that the payment_type parameter has one of the allowable values.

Line 22 tries to find a user with the given contract id.  If one is found, a thank-you email is sent to the user. If one isn't found, an error is returned to the client.  I don't use an explicit return here because the control flow will result in exiting from the method regardless, so there is no risk of a double-rendering error.

The RSpec tests might look like:

 1|require "spec_helper"

 2|describe Api::V1::SelfCare::ReceiptsController do

 3|  before do
 4|    User.create!(:contract_id => '112233', :email => '')
 5|    #request.env["HTTP_ACCEPT"] = 'application/json'
 6|    #request.env["HTTP_CONTENT_TYPE"] = 'application/json'
 7|  end

 8|  describe "POST #payment_posted" do
 9|    params_base = {
10|      contract_id: '112233',
11|      amount: 30.10,
12|      payment_type: 'CREDIT'
13|    }
14|    let(:params) { params_base.dup }

15|    context "with valid attributes" do
16|      before :each do
17|        post :payment_posted, params, :format => :json
18|      end

19|      it "should successfully return content type as application/json" do
20|        response.content_type.should eq('application/json')
21|      end

22|      it "should return success within the body" do
23|        response.body.should include('"success":true')
24|      end

25|      it "should return status 201" do
26|        response.status.should eq(201)
27|      end

28|    end


Notice the commented out JSON request header settings on line 5-6.  They aren't necessary, since the JSON request format is specified on line 17, which has the equivalent effect.

The tests are testing anything that complicated, just that the result has the expected JSON body, the right HTTP status, and the right content type.  Of course, these tests aren't comprehensive, but they illustrate the types of things to test for and the syntax/commands to do it.

I also like to use CURL, which is a command line network utility that can make may types of network requests, including HTTP requests, with and without SSL.  With CURL, you can set HTTP headers, set cookie values, and many other things.

CURL is client platform and server platform agnostic.  It's a great tool to use to see if an API works as advertised or to demonstrate to others that your API is working as advertised.

Here is a CURL invocation to test the API:

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d ' {"contract_id":"123456", "amount": 30.95, "payment_type": "CREDIT"}' http://localhost:3000/api/v1/self_care/payment_posted

In this case, I've set two HTTP headers to specify the JSON type of this request.  I's also specified POST parameters using the -d option.  Finally, I've specified the API endpoint URL, which in this case has domain localhost:3000 because I am testing an app running locally on my machine.

If you want to get this running, you'll also need this snippet for your routes.rb file:

namespace :api do
  namespace :v1 do
    namespace :self_care do
      post '/payment_posted', :to => 'receipts#payment_posted'

That's about it for this quick tour of JSON API's in Rails. For more info, I recommend "Rails 3 In Action" by Yehuda Katz and Ryan Bigg.  Most of the book uses the Cucumber testing framework, but the chapter on API's uses Rspec as I've done above.

Views: 687


You need to be a member of TechHui to add comments!

Join TechHui


web design, web development, localization

© 2022   Created by Daniel Leuck.   Powered by

Badges  |  Report an Issue  |  Terms of Service