Skip to content

Instantly share code, notes, and snippets.

@oojikoo-gist
Created April 12, 2015 19:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save oojikoo-gist/9a2a26aa8224ee41ba2e to your computer and use it in GitHub Desktop.
Save oojikoo-gist/9a2a26aa8224ee41ba2e to your computer and use it in GitHub Desktop.
rails: restful api basecontroller

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

Some Routing

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

Next Steps

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.

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:

@chienkira
Copy link

nice metaprogramming hack!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment