Progress isn’t made by early risers. It’s made by lazy men trying to find easier ways to do something ― Robert Heinlein

There are many issues that developers face and have to overcome during developing large systems. Here at MdG, it is a daily experience and besides solving them, we try to learn from these complexities

Usually, when you are developing an API there are different features that you have to implement like Authentication, File Uploads, Background Jobs, etc. But there is also boilerplate code and repetition in most RESful controllers.

We decided to come up with a better way of writing RESTful controllers.

Rails Request/Response Lifecycle

When your web app receives a request, the router will decide which controller and action to run, then Rails creates an instance of that controller and runs the method with the same name as the action. For example, if the user visits /posts/new inside the web application, Rails will create an instance of PostsControllerand calls its new method.

As we know from inheritance in Ruby if you call a method on the object, Ruby looks for the method on the class of that object, next it looks for the method in one of the parent classes, if not found Ruby will start another search from the object and tries to locate method_missing, and if method_missing is not found It will throw a NoMethodError.

So we took advantage of Ruby inheritance lookup on our controllers.

We decided to have a Base API Controller which implements all the RESTful actions, and other RESTful controllers can inherit from it.

Inheritance of RESTful Controllers

Using the right ActiveRecord model insideBaseApiController actions

If child controllers followed the Rails convention each of them should have a model with the same name as theirs, but in singular form(ex. for PostsController.rb it should be a Post).

First, we needed the name of the controller that was currently using the actions of the BaseApiController. In Rails, every controller extends ApplicationController but the real functionality resides inside ActionController:: Base. So in Rails docs about ActionController:: Base there, we found some useful methods.

Inside ourBaseApiController.rb we callcontroller_name(provided by ActionController) to get the name of the current child controller and chain singularizeto make it singular.

The final result is a Stringpost:

[1, 9] in /media/azdren/AZDREN/rails-projects/blog-app/app/controllers/base_api_controller.rb
   1: # frozen_string_literal: true
   2: 
   3: class BaseApiController < ApplicationController
   4:   #   GET /posts/hello
   5:   def hello
   6:     byebug
=> 7:     render plain: 'Hello there'
   8:   end
   9: end
(byebug) self.controller_name
=> "posts"
(byebug) self.controller_name.singularize
=> "post"

Next, we chain another method classify ( ActiveSupport)which internally is used by Rails to convert plural string table names into singular models. Finally, to convert it into a class we call constantize (ActiveSupport) which tries to find a constant with a given name.

Result:

[1, 9] in /media/azdren/AZDREN/rails-projects/blog-app/app/controllers/base_api_controller.rb
   1: # frozen_string_literal: true
   2: 
   3: class BaseApiController < ApplicationController
   4:   #   GET /posts/hello
   5:   def hello
   6:     byebug
=> 7:     render plain: 'Hello there'
   8:   end
   9: end
(byebug) self.controller_name.classify.constantize
=> Post(id: integer, created_at: datetime, updated_at: datetime)

The final code listing:

Applying a before_action filter

On line 4 we apply a before_action filter which makes sure that before running any of the actions we create our model class based on the controller name.

 # frozen_string_literal: true 
 
 class BaseApiController < ApplicationController
   before_action :class_model

class_model method:

def class_model
  @class_model ||= res_name.classify.constantize
end

res_name method:

def res_name
   @res_name ||= self.controller_name.singularize
end

Index action

Inside index action, we call @class_model.all and store the result on data variable. Next render_json_data is called which sets the data to an instance variable named after model and renders it as JSON.

def index
	data = @class_model.all # similar to User.all or Post.all
	render_json_data(data)
end

render_json_data method:

def render_json_data(data)
  render json: instance_variable_set("@#{res_name.pluralize}", data)

Show action

On the show action instead of getting all the data we look for one record provided by the id parameter :

def show
  data = @class_model.find(params[:id])
  render_json_data(data)
end

Create action

Create action is a bit more complex. First, we call set_res and inside it, we create an instance of the model with res_params passed on the constructor. Next set_res checks if the argument is nil and if it is, it runs another query that tries to get the model based on its id parameter. In the end, it assigns the data to a new instance variable with the name of the model. Finally, we call created_res.save which saves the data. If the model gets saved we render it otherwise we render its errors.

def create
   set_res(class_model.new(res_params))
   
   if created_res.save
     render_json_data(created_res)
   else
     render_json_data(created_res.errors.full_messages)
   end
end

set_res method:

def set_res(res = nil)
    res ||= class_model.find(params[:id]) # Post.find(params[:id])	
    instance_variable_set("@#{res_name}", res)
end

created_res method:

def created_res
  instance_variable_get("@#{res_name}")
end

res_params method:

def res_params
  @res_params ||= self.send("#{res_name}_params")
end

Update action

This action is kind of similar to create action with a small difference. Instead of trying to save the object, we call the update method:

def update
   set_res(class_model.find(params[:id]))

   if created_res.update(res_params)
     render_json_data(created_res)
   else
     render_json_data(created_res.errors.full_messages)
   end
end

Destroy action

Same thing with destroy action instead of saving we delete the data and return a response:

def destroy
   set_res(class_model.find(params[:id]))

   if created_res.delete
     render_json_data({ message: 'Successfuly deleted', status: 200 })
   else 
     render_json_data(created_res.errors.full_messages)
   end
end

Github Repo:

github.com/montedelgallo/base_api_controller

Resources:

Action Controller Overview, Ruby Inheritance, ActionController::Base, controller_name, singularize, classify, constantize, Generic Rails Controllers.