Fighting Custom Actions in Rails Controllers
In this post, we'll go over creating custom actions in Rails controllers, and when it is appropriate, and not appropriate, to use them in your Rails code.
Join the DZone community and get the full member experience.Join For Free
I have never seen a Rails project which doesn't have custom actions in the Rails controllers. And that makes me upset. Here I'm going to describe my thoughts about custom actions, why people use them, why I hate them, and how to deal with them.
Let's say we want to build a simple posting system. We have already created a model Post and need to add a controller. Everybody knows about REST and we obviously want to build a RESTful application. So we create a controller:
class PostsController < ApplicationController def index ... end def show ... end ... def destroy .. end end
resources :posts to routes.rb
We can make it even easier and faster using the Rails generators:
rails g scaffold Post title and
As a result, we have a RESTful controller with seven common actions. Now users can add a new post, review existing posts, etc. Later on, we want users to be able to like posts. We find a great gem called
acts_as_votable, follow the instructions, and add the gem to the project. Now we think: "ok, there is already a PostsController and the user can like a Post. It's just another action that the user can perform on a Post so it makes sense to keep all actions related to the Post within PostsController," and add a custom action.
class PostsController < ApplicationController ... def like @post = Post.find(params[:id]) @post.liked_by current_user end end
resources :posts do member do post :like endend to routes.rb
Great, it's a nice, tiny action and now the user is able to like posts. Cool? NO!
We do like a post which seems like it's performing an action on a post but in fact, we create another resource -
like. It's not so obvious right now but later on when we'll need to add other actions (e.g. dislike, get all likes, etc.) it will become much easier to see. And we'll see that all of those actions are RESTful actions with the
like resource. So the right approach would be to create a separate RESTful controller -
LikesController. It makes, even more, sense to make it a nested resource of a post.
class LikesController < ApplicationController ... def create @post = Post.find(params[:id]) @post.liked_by current_user end def index @likes = @post.get_likes end ... private def find_post @post = Post.find(params[:post_id]) end end
In routes.rb, add:
resources :posts do resources :likesend
As a result, we have two resources, two RESTful controllers, and no custom actions. I found that this mistake is usually made by new RoR developers and this one is far from the only case. Let's take a look at another example.
Let's say we are building an API for an events management system. We have a controller that is very much like a PostsController.
class EventsController < ApplicationController def index ... end def show ... end ... def destroy ... end end
resources :events to routes.rb. And we use ActiveModelSerializers to generate JSON objects.
The API works on behalf of a user and now we need to generate JSON for events that belong only to a user. This time, we think: "ok, last time we had a totally different resource 'like,' but now we are working with exact events, so it definitely must be in EventsController."
class EventsController < ApplicationController ... def my_events json = ActiveModel::ArraySerializer.new( current_user.events, each_serializer: MyEventSerializer, root: nil) render json: json, status: :ok end end class MyEventSerializer < ActiveModel::Serializer attributes :id, :title, :descriptionend In routes.rb resources :events do collection do get :my_events end end
If "my events" JSON is just the same as "all events" JSON then it's worth thinking about some sort of filtering ability to the
events_controller#index action. In our case, we added a separate serializer which means that "my events" JSON is different from "all events" JSON. I can bet that later MyEventsSerializer will change so often that "my event" object will be very different from "event" object, though they are both stored in one DB table. Later we'll also need to delete own events, etc. This often happens because a user has more permissions with his own events. It's not so obvious at first, but
my_event are separate resources. Even if they both use the same model we have no idea how they are used by mobile apps or web apps. It's very likely that mobile apps have different classes to wrap our JSON, and it doesn't matter if they use inheritance since they are instances of different classes. So it makes much more sense to simply create a separate controller in our Rails API and extract all data related to "my event" from that controller.
class MyEventsController < ApplicationController def index json = ActiveModel::ArraySerializer.new( current_user.events, each_serializer: MyEventSerializer, root: nil) render json: json, status: :ok end def show render json: MyEventSerializer.new(@event), status: :ok end ... end
resources :eventsresources :my_events, only: [:index, :destroy, :show]
There are many cases when developers add custom actions to Rails controllers, and as their controllers become larger, their application grows. A lot of stuff begins to happen in a controller and you think "why is it here? Why do we create books in the AuthenticationController?" I made a simple rule for myself: there shouldn't be custom actions in the RESTful application. Any custom action in one controller is a RESTful action in another.
When you are adding the eighth action to your controller think to yourself, "maybe it makes more sense just to extract it to a separate controller?" Don't be lazy and create additional controllers! Don't overload controllers! Every controller should manage only its own resource!
Published at DZone with permission of Dasha Zymina. See the original article here.
Opinions expressed by DZone contributors are their own.