Ruby on Rails: DRY controllers with responders gem
Note (Feb 4, 2021): Over time I’ve become increasingly convinced that responders, while decreasing the amount of boilerplate in your controller code, hurt more than they help. Focusing on decreasing the amount of controller code you write in general is a better idea. Rails’ magic is excellent most of the time, but there’s a reason that Rails removed this concept and extracted it into its own gem. Now that I’ve given you my own opinion, feel free to act on the contents of this post as you see fit!
Rails’ ability to generate scaffolding is nice, but the way the resulting controllers are structured seems to abandon some part of the ‘thin controller, fat model’ ideology (that said, fat models are still not great). Generating a scaffold for a User
model gives us a controller which handles action responses in this way:
# app/controllers/users_controller.rb
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: 'User was successfully created.' }
format.json { render :show, status: :created, location: @user }
else
format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
This action alone occupies 10 lines of the entire file’s 74. While it might be easy to read and understand what is going on, it seems like there’s a huge opportunity to DRY things up. Not only does this look a unnecessarily large immediately after generating the scaffold, it only gets worse as code complexity increases. Handling responses can quickly become more complicated than “if the record is saved respond like this, otherwise respond like this”. While this could be a sign that your controller (and by extension, everything else) could be in need of refactoring, it could only benefit us to have an easier way to dictate how we respond.
This is where the responders gem comes in handy. The author has bundled a controller generator with the gem, which can be activated by adding the following line to config/application.rb
:
# config/application.rb
config.app_generators.scaffold_controller :responders_controller
Let’s take a look at the same controller action, but using our new scaffold generator:
# app/controllers/users_controller.rb
respond_to :html
def create
@user = User.new(user_params)
@user.save
respond_with(@user)
end
This result is much clearer. At the top of the controller, we define a standard response format, in this case just :html
, but just as easily a list like :html, :json, :js
. The responders
generator leaves off the json
reponse by default, but you only need to add :json
to the respond_to
call. The action itself is now 3 lines long - set up the User
, save it, and respond with the User
we just saved. respond_with
responds based on the state of @user
. This way, we avoid the biggest problem with the default layout - the if/else causing the code to repeat itself.
Initially you might notice we’ve lost a bit of functionality, but nothing we can’t add back if we do need it. It will respond with whatever formats we’ve defined using respond_to
, exactly as it would have previously. If we’d like to include notice messages like before, we can do that - let’s add the :json
response back at the same time:
# app/controllers/users_controller.rb
respond_to :html, :json
responders :flash
# config/locales/en.yml
en:
flash:
actions:
create:
notice: "%{resource_name} was successfully created."
alert: "%{resource_name} could not be created."
update:
notice: "%{resource_name} was successfully updated."
alert: "%{resource_name} could not be updated."
destroy:
notice: "%{resource_name} was successfully destroyed."
alert: "%{resource_name} could not be destroyed."
At first glance, this might seem like a lot of work - but this only needs to be done once. This is more convenient though - we now have one central place to change all of these messages, which apply to all models. If we want to override this for a particular model, we can do that too:
# config/locales/en.yml
flash:
users:
create:
notice: "The account was succcessfully created."
alert: "The account could not be created."
With these changes we’ve made, our controllers are much smaller - users_controller.rb
was 74 lines, now 48 - with no loss in functionality.
If you’d like to provide a custom redirect path, that is as simple as providing a location
:
# app/controllers/users_controller.rb
respond_with(@user, location: users_path)
It’s as simple as that. May your bugs be squashed and your controllers, dry. Happy hacking!