How To Implement Rails APIs Like A Pro

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.

15 thoughts on “How To Implement Rails APIs Like A Pro

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

  2. 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 ‘

Leave a reply to Jaron Gao Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.