DRY Your JSON APIs with Rails

Making an API in rails can become very ugly if not done correctly.

Making an API in rails can become very ugly if not done correctly.

Let's take an example of a blogging API with the following requirements:

  • Authentication
  • CRUD operations
  • Useful error messages

Authorization

Let's create a controller to view all blog posts, and a specific blog post, for the current user. Our first pass looks like this:

class PostsController < ActionController::Base
  def index
    @user = User.find_by(api_key: request.authorization)
    if [email protected]
      return render json: {error: 'invalid api key'}, 
        status: :unauthorized
    end
    @posts = @user.posts
    # ... render JSON
  end
  
  def show
    @user = User.find_by(api_key: request.authorization)
    # ... validate user
  end
end

The @user = User.find_by(...) is repeated twice, and will be repeated every time we add a new controller method. Even worse, this code will be copy-pasted around for every new controller we introduce!

Let's move it to a new ApiController class that will be the base for our future controllers:

class ApiController < ActionController::Base
  abstract!
  attr_reader :current_user
  before_action :find_current_user
  
  protected
  
  def find_current_user
    @current_user = User.find_by(api_key: request.authorization)
    unless @current_user
      render json: { error: 'Cannot find user by API key' }, 
        status: :unauthorized
    end
  end
end

We also followed some best practices while here:

  1. Named the currently logged in user as current_user. This is a defacto convention in a lot of authorization frameworks, such as Devise
  2. Flagged the ApiController as abstract! to clearly indicate that it should be inherited from, and not directly called.

Finally, our original code now looks like this:

class PostsController < ApiController
  def index
    @posts = current_user.posts
    render json: @posts.to_json
  end
end

Re-Usable Views

An easy way to render your models is to use the to_json method. However, it can prove to be painful if you wish to use only specific fields, especially if they're nested.

For example, say we want to return only certain fields for a post, we could use to_json:

class PostsController < ApiController
  def index
    @posts = current_user.posts
    render json: @posts.to_json(only: [:id, :title, :description]})
  end
  
  def show
    @post = current_user.posts.find_by(params[:id])
    # todo: check @post is not null
    render json: @post.to_json(only: [:id, :title, :description]})
  end
end

A problem with the above is that the to_json is repeated for every view.

The solution is to bring the V back into MVC for your APIs using a tool like jbuilder. You can define your post JSON as a partial, and each index/show method renders the partial:

# app/views/posts/_post.jbuilder
json.(@post, :id, :title, :description)

# app/views/posts/index.jbuilder
json.array! @posts, partial: 'posts/post' as: post

# app/views/posts/show.jbuilder
json.partial! 'posts/post', @post

Now your API code is significantly cleaner:

class PostsController < ApiController
  def index
    @posts = current_user.posts
  end
  
  def show
    @post = current_user.posts.find_by(params[:id])
  end
end

DRY UP YOUR CRUD WITH RESCUE_FROM

The traditional flow of rails model saving results in API code that looks a bit like this:

class PostsController < ApiController
  def update
    @post = current_user.posts.find_by(id: params[:id])
    unless @post
      return render json: { error: 'Unable to find blog post' }, 
        status: :not_found
    end
    if [email protected]_attributes(param[:post])
      return render json: { 
        error: "Could not update post",
        validation: @post.errors,
      }, status: :unprocessable_entity
  end
end

By using the rescue_from method and the ! model methods, you can significantly DRY up your code. Let's add to our ApiController:

class ApiController < ApplicationController::Base
  
  rescue_from ActiveRecord::RecordNotFound do |e|
    render json: { error: e.message }, status: :not_found
  end
  
  rescue_from ActiveRecord::RecordInvalid do |invalid|
    # You can even use jbuilder templates to make this cleaner!
    render 'shared/record_invalid', 
      locals: { exception: invalid },
      status: :unprocessable_entity
  end
end

Now the rest of your controllers can become even DRYer:

class PostsController < ApiController
  def update
    @post = current_user.posts.find(params[:id])
    @post.update_attributes!(params[:post])
  end
  
  def create
    @post = current_user.posts.create!(params[:post])
  end
end

Exceptions for Flow Control

This approach uses exceptions as a means for flow control. Some people may argue that it's an antipattern, but others argue that it may be acceptable in some circumstances, especially if the language or framework encourages it.

In my opinion, I think using exceptions as flow control like this to really simplify your code is worth it. For example, say you have some code outside of your controllers trying to find something that doesn't exist:

@post.tags.find!(name: 'Ruby on Rails')

Because of this rescue_from, if the Ruby on Rails tag is not found, then the API now returns a 404 instead of a 500, with virtually little extra work on your part.

Regardless, I think it's best to keep the number of rescued exceptions minimal – and never rescue specific, generic exception classes. For example, rescuing a IOError and casting it as a 403 is a really bad idea, as it will be very difficult to debug.

Better Error Messages with StrongParameters

For DRY and useful API error messages, you can use StrongParameters and rescue_from to your advantage:

class ApiController < ActionController::Base
  rescue_from ActionController::ParameterMissing do |e|
    # You can even render a jbuilder template too!
    render json: {error: e.message }, status: :unprocessable_entity
  end
end

And now in your other controllers:

class PostsController < ApiController  
  def create
    @post = current_user.posts.create!(params.require(:post))
  end
end

Now your end user will automatically be given a descriptive error message if they forget a parameter!

Share this article: Link copied to clipboard!

You might also like...

Secure your API Keys with JWT

DRY Your JSON APIs with Rails