Implementing Rails APIs like a professional

Today, a web-portal that does not support APIs is not even considered a web portal! This post explains how we can implement Rails APIs in our application. Rails APIs require the following minimum implementation:

  • API keys.
  • API Versioning.
  • API Request Authentication.
  • Request throttling (optional).

Let’s see how this can be done.

API key generation

Lets generate the model.

$ rails g model User api_key:string

or if the model already exists,

$ rails g migration AddApiKeyToUsers api_key:string

Now lets edit this model and add the logic to generate the API key when a record is created using the method generate_api_key.

def generate_api_key
  loop do
    token = SecureRandom.base64.tr('+/=', 'Qrt')
    break token unless User.exists?(api_key: token).any?
  end
end

In this code we use SecureRandom.base64.tr('+/=', 'Qat') to generate a token. Since, it may contain the special characters like +/=, we shall replace them with some random alphabets using tr, so that the API key looks good. Now lets add the before_create callback.

before_create do |doc|
  doc.api_key = doc.generate_api_key
end

API controller and routes versioning

Now, lets generate the controller called Api in the api namespace. This helps us avoid the ApplicationController as APIs do not need the basic controller functionalities like protection from request forgery (the CSRF token), session authentication and the view helpers. Our Api::ApiController is in fact the ApplicationController for all our API controllers.

$ rails g controller api/api

To ensure we get basic controller functionality we need our Api::ApiController to inherit directly from ActionController::Base. So, be sure to replace the code as follows:

class Api::ApiController < ActionController::Base
# Authentication and other filters implementation.
end

Lets take an example to help understand all this better. Lets create an API for events that has two fields name and occurred_at that tell us which event occurred at which time. We shall write APIs to get events.

$ rails g model event name:string occurred_at:time
$ rails g controller api/v1/events

APIs need to be versioned to ensure backward compatibility and future extensibility. The above approach provides us an easy way to do this and keep our code maintainable. Remember to version only when required – v2 would be added when either there is entirely new functionality that would not work with v1 clients, i.e. potentially broken compatibility. (This may be a good time to mention jsonapi.org. This is what the next generation of Web APIs will do, so it’s definitely worth a read).

# app/controllers/api/v1/events_controller.rb
class Api::V1::EventsController < Api::ApiController
end

Routes versioning

This is an art and you should ensure that your routes are well designed. Here is an example.

namespace :api do
  namespace :v1 do
    resources :events
  end
end

This will generate the routes with ‘api/v1’ namespace prefix.

Prefix            Verb   URI Pattern                       Controller#Action
api_v1_events     GET    /api/v1/events(.:format)          api/v1/events#index {:format=> :json}
                  POST   /api/v1/events(.:format)          api/v1/events#create {:format=> :json}
new_api_v1_event  GET    /api/v1/events/new(.:format)      api/v1/events#new {:format=> :json}
edit_api_v1_event GET    /api/v1/events/:id/edit(.:format) api/v1/events#edit {:format=> :json}
api_v1_event      GET    /api/v1/events/:id(.:format)      api/v1/events#show {:format=> :json}
                  PATCH  /api/v1/events/:id(.:format)      api/v1/events#update {:format=> :json}
                  PUT    /api/v1/events/:id(.:format)      api/v1/events#update {:format=> :json}
                  DELETE /api/v1/events/:id(.:format)      api/v1/events#destroy {:format=> :json}

If want to remove the API prefix (/api) and use a subdomain like api.example.com, then we can use options like path and constraints

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

Using path option we can change the namespace name. Now routes look like this

Prefix            Verb   URI Pattern                   Controller#Action
api_v1_events     GET    /v1/events(.:format)          api/v1/events#index {:format=> :json, :subdomain=> "api"}
                  POST   /v1/events(.:format)          api/v1/events#create {:format=> :json, :subdomain=> "api"}
new_api_v1_event  GET    /v1/events/new(.:format)      api/v1/events#new {:format=> :json, :subdomain=> "api"}
edit_api_v1_event GET    /v1/events/:id/edit(.:format) api/v1/events#edit {:format=> :json, :subdomain=> "api"}
api_v1_event      GET    /v1/events/:id(.:format)      api/v1/events#show {:format=> :json, :subdomain=>"api"}
                  PATCH  /v1/events/:id(.:format)      api/v1/events#update {:format=>:json, :subdomain=>"api"}
                  PUT    /v1/events/:id(.:format)      api/v1/events#update {:format=> :json, :subdomain=> "api"}
                  DELETE /v1/events/:id(.:format)     api/v1/events#destroy {:format=>:json, :subdomain=> "api"}

If we want to process every request as a JSON request by default, then add format: :json to the defaults in the namespace options. So any request will now be considered as a JSON request and will give JSON response.

namespace :api, path: '', constraints: {subdomain: 'api'}, defaults: {format: 'json'}

Here is an example of our API controller action.

class Api::V1::EventsController < Api::ApiController
  respond_to :json

  def index
    @events = Event.all
    respond_with @events
  end
end

API Authentication

This is important and we can implement the authentication in the Api::ApiController itself so that it is applicable for all the API controllers. There are different ways to authenticate:

  • Use the inbuilt rails method authenticate_or_request_with_http_token
  • Do it manually.

If we use authenticate_or_request_with_http_token(ref: token auth api), our code looks like this.

class Api::ApiController < ActionController::Base
  private

  def authenticate
    authenticate_or_request_with_http_token do |token, options|
      @user = User.where(api_key: token).first
    end
  end
end

Now, in our EventsController we need to ensure we add the filter. (Note: Rails3 would require this to be before_filter).

class Api::V1::EventsController < Api::ApiController
  respond_to :json

  before_action :authenticate

  def index
    @events = Event.all
    respond_with @events
  end
end

Now, to test this API out we can use curl.

$ curl -H "Authorization: Token token=WwEsPpOCYMsyPsmmvKBqQDOaEJ4t" http://localhost:3000/events

If we pass the api_key in the request header (i.e the X-API-KEY header field), then we need to do the authentication manually.

def authenticate
  api_key = request.headers['X-Api-Key']
  @user = User.where(api_key: api_key).first if api_key

  unless @user
    head status: :unauthorized
    return false
  end
end

Again, we can test this with curl.

$ curl -H "X-Api-Key: WwEsPpOCYMsyPsmmvKBqQDOaEJ4t" http://localhost:3000/v1/events.json

NOTE 1 Never pass the API key in the params(for example: http://test.com?api_key= WwEsPpOCYMsyPsmmvKBqQDOaEJ4t or in the POST or PUT parameters). The params will get logged and the API keys would be seen in the logs. If someone has access to your logs or purges the logs or even by mistake pastes a log snippet on stackoverflow, then God help you! We can of course use filtering configuration in our application but the best way is to avoid this situation and pass the API key in the request header always.

NOTE 2 Always use the HTTP status codes for different response status to make the HTTP status code more readable.

render status: :unauthorized # 401
render status: :too_many_requests # 424
render status: :unprocessable_entity # 422
# and many more!

In my next post, I shall talk about Request throttling. The information here is enough for you to get started on writing good Rails APIs that are versioned, clean and authenticated. If you want to see a sample working code, see Github.

Feedback is welcome.

Advertisements
This entry was posted in Ruby on Rails, Tutorials and tagged , , , . Bookmark the permalink.

15 Responses to Implementing Rails APIs like a professional

  1. Pingback: API Throttling on Requests Per Minute | Josh Software – Where Programming is an Art!

  2. For most up-to-date information you have to visit internet and on the web I found this website as
    a most excellent website for hottest updates.

  3. Legacy Devise User says:

    Thanks for the link to a github example! Going to give this a try since Devise has dropped support for tokenized auth 😦

  4. thejkfever says:

    Great post, thanks for the clarity on security with passing Auth tokens in request headers.

  5. sakthivel says:

    Hi Am getting following error when I follow this blog…, Please help me out

    NoMethodError in Devise::RegistrationsController#create
    undefined method `exists’ for #

    Extracted source (around line #36):

    loop do
    token = SecureRandom.base64.tr(‘+/=’, ‘Qrt’)
    break token unless User.exists ? api_key: token.any?
    end
    end

    Rails.root: /home/ubuntu/workspace/sample_app

    Application Trace | Framework Trace | Full Trace
    app/models/user.rb:36:in `block in generate_api_key’
    app/models/user.rb:34:in `loop’
    app/models/user.rb:34:in `generate_api_key’
    app/models/user.rb:30:in `block in ‘

  6. Great post!!!

    One question: when do you pass de token to the user client? In the response after a successful login?

  7. Shazad Maved says:

    and does the token change on each sign ?

  8. Rich Seviora says:

    User.exists?(api_key: token) will return a boolean value. ‘any?’ should not be called on the result of exists?.

    Documentation: http://apidock.com/rails/ActiveRecord/FinderMethods/exists%3F

    Otherwise, helpful πŸ™‚

  9. Jon says:

    Is this secure for large-scale application? If not, what should I use to accomplish this?

  10. Pingback: Implementing Rails APi | My Blog

  11. Jaron Gao says:

    Nice overview! Thank you for writing this.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s