Restful Rails API, Just Add Water

I make a habit out of hunting for fun new web applications and services across the interwebs. These applications can be very different but one thing that does not change is the need for them to have a decent API. When I find a service that has a nonexistent, a poorly documented, or an inconsistent API, I cringe. No matter how amazing your application is, it will never reach the next level without being able to easily integrate with other services.

Instead of just complaining about the problem I thought I would take the time to teach others how to easily get up and running with a solid, consistent, and easily extensible Restful JSON API with Ruby on Rails.

image

The guide will assume that we are dealing with a pre-existing application that has two models: Album and Artist. An album belongs to an artist and an artist has many albums.

Requirements

This guide is for Rails 4.0.0+ only.

These gems can always be replaced with alternatives, but they will be good for demonstration. Add the following gems to your Gemfile:

gem 'jbuilder' # used for serialization of models into JSON  
gem 'kaminari' # adds pagination to ActiveModels  

Controllers

Now let's get into the nitty-gritty. We are going to create the file app/controllers/api/base_controller.rb to encapsulate the majority of our API logic. Copy and paste the following:

module Api  
  class BaseController < ApplicationController
    protect_from_forgery with: :null_session
    before_action :set_resource, only: [:destroy, :show, :update]
    respond_to :json

    private

      # Returns the resource from the created instance variable
      # @return [Object]
      def get_resource
        instance_variable_get("@#{resource_name}")
      end

      # Returns the allowed parameters for searching
      # Override this method in each API controller
      # to permit additional parameters to search on
      # @return [Hash]
      def query_params
        {}
      end

      # Returns the allowed parameters for pagination
      # @return [Hash]
      def page_params
        params.permit(:page, :page_size)
      end

      # The resource class based on the controller
      # @return [Class]
      def resource_class
        @resource_class ||= resource_name.classify.constantize
      end

      # The singular name for the resource class based on the controller
      # @return [String]
      def resource_name
        @resource_name ||= self.controller_name.singularize
      end

      # Only allow a trusted parameter "white list" through.
      # If a single resource is loaded for #create or #update,
      # then the controller for the resource must implement
      # the method "#{resource_name}_params" to limit permitted
      # parameters for the individual model.
      def resource_params
        @resource_params ||= self.send("#{resource_name}_params")
      end

      # Use callbacks to share common setup or constraints between actions.
      def set_resource(resource = nil)
        resource ||= resource_class.find(params[:id])
        instance_variable_set("@#{resource_name}", resource)
      end
  end
end  

This may look a little foreign at first and it should, it uses some less common metaprogramming techniques to provide functions that reduce duplication across our code.

What are those?

get_resource: provides us with what would normally be our instance variable; eg @artists or @albums, and returning us with it's value.

set_resource: sets the instance variable that get_resource retrieves.

resource_class: returns the class of the model that we are currently working with, it is infered from the controller's name.

resource_name: is just the name of the resource that we're referring to same as resource_class but instead of the class Album it is the string "album".

resource_params: calls the resource specific params method of a child controller, eg album_params.

page_params: allows us to define permitted page-related parameters that will be inherited by all of our API controllers. I find this very useful for allowing pagination of data.

query_params: acts mostly as a place holder to allow for quick extension of direct-matching queries on whitelisted attributes anything past direct-matching requires custom logic.

Back to the logic

Next you will want to add the public resource methods to the same controller:

# POST /api/{plural_resource_name}
def create  
  set_resource(resource_class.new(resource_params))

  if get_resource.save
    render :show, status: :created
  else
    render json: get_resource.errors, status: :unprocessable_entity
  end
end

# DELETE /api/{plural_resource_name}/1
def destroy  
  get_resource.destroy
  head :no_content
end

# GET /api/{plural_resource_name}
def index  
  plural_resource_name = "@#{resource_name.pluralize}"
  resources = resource_class.where(query_params)
                            .page(page_params[:page])
                            .per(page_params[:page_size])

  instance_variable_set(plural_resource_name, resources)
  respond_with instance_variable_get(plural_resource_name)
end

# GET /api/{plural_resource_name}/1
def show  
  respond_with get_resource
end

# PATCH/PUT /api/{plural_resource_name}/1
def update  
  if get_resource.update(resource_params)
    render :show
  else
    render json: get_resource.errors, status: :unprocessable_entity
  end
end  

Now that we have the generic API logic setup we just need to connect it to our model controllers. Pay attention that these inherit from Api::BaseController. In app/controllers/api/albums_controller.rb:

module Api  
  class AlbumsController < Api::BaseController

    private

      def album_params
        params.require(:album).permit(:title)
      end

      def query_params
        # this assumes that an album belongs to an artist and has an :artist_id
        # allowing us to filter by this
        params.permit(:artist_id, :title)
      end

  end
end  

In app/controllers/api/artists_controller.rb:

module Api  
  class ArtistsController < Api::BaseController

    private

      def artist_params
        params.require(:artist).permit(:name)
      end

      def query_params
        params.permit(:name)
      end

  end
end  

From here if you need custom functionality in a specific model's controller simply include a method with the same name and define your overriding functionality.

Some Routing

Now we actually need to route stuff to our new fancy API controllers! Add this to config/routes.rb:

namespace :api do  
  resources :albums, :artists
end  

You may have noticed that I did not nest these routes, while this can allow for more meaningful url paths I personally feel it makes it much harder to produce consistent API endpoints. I would prefer for the AlbumController to allow a param for artist_id that we can then filter on. Another benefit for this is it allows for easier integration with techonologies like the Ember.js REST data adapter, which assumes non-nested routes.

Serializing Data

So that's great, now things are actually going to the correct controllers, but at the moment it's just spitting out our full models as JSON, that's not really what we want. We want to control what it shows. This is where jbuilder comes in handy.

In app/views/api/albums/index.json.jbuilder:

json.albums @albums do |album|  
  json.id    album.id
  json.title album.title

  json.artist_id album.artist ? album.artist.id : nil
end  

In app/views/api/albums/show.json.jbuilder:

json.album do  
  json.id    @album.id
  json.title @album.title

  json.artist_id @album.artist ? @album.artist.id : nil
end  

In app/views/api/artists/index.json.jbuilder:

json.artists @artists do |artist|  
  json.id   artist.id
  json.name artist.name
end  

In app/views/api/artists/show.json.jbuilder:

json.artist do  
  json.id   @artist.id
  json.name @artist.name
end  

Now just add some data to your database using the rails console or maybe try adding something through a POST to your new lovely JSON API. I use the app Rested from the Mac App Store for manually testing API endpoints.

Next Steps

While it's all good fun having an API, some serious security and performance concerns come into play. Some next steps for making a production ready API and possible future blog topics include:

  • Use fragment caching to make your API efficient. jbuilder offers advantages in caching over libraries like activemodelserializers because you can cache JSON templates the same way you would erb templates.
  • Secure your API, gems that we use everyday include CanCan and Devise to offer per user permissions on resources.
  • Include some more complex functionality like side-loading for convenience in end-user application development.

Update: This article has gotten a lot of attention and others have used it in their projects, I highly encourage you to checkout these examples of use to continue your learning: