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.
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.
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
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.
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.
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.
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.
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.
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:
erb
templates.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: