Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Fighting Custom Actions in Rails Controllers

DZone's Guide to

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.

· Web Dev Zone
Free Resource

Make the transition to Node.js if you are a Java, PHP, Rails or .NET developer with these resources to help jumpstart your Node.js knowledge plus pick up some development tips.  Brought to you in partnership with IBM.

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


Also, add resources :posts to routes.rb
We can make it even easier and faster using the Rails generators:  rails g scaffold Post title and  string description:text.

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

And add 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

Also, add 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

Cool? NO!

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 event  and 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

Our routes.rb:
 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!

Learn why developers are gravitating towards Node and its ability to retain and leverage the skills of JavaScript developers and the ability to deliver projects faster than other languages can.  Brought to you in partnership with IBM.

Topics:
ruby on rails ,ruby on rails developers ,web dev

Published at DZone with permission of Darya Kuritsyna. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}