This article is also available in Japanese. すごい!
In a recent interview with Full Stack Radio DHH explains how he organizes Rails controllers in the latest version of Basecamp. Here is an enhanced transcript:
What I’ve come to embrace is that being almost fundamentalistic about when I create a new controller to stay adherent to REST has served me better every single time. Every single time I’ve regretted the state of my controllers, it’s been because I’ve had too few of them. I’ve been trying to overload things too heavily.
So, in Basecamp 3 we spin off controllers every single time there’s even sort of a subresource that makes sense. Which can be things like filters. Like, say, you have this screen and it looks in a certain state. Well, if you apply a couple of filters and dropdowns to it, it’s in a different state. Sometimes we just take those states and we make a brand new controller for it.
The heuristics I use to drive that is: whenever I have the inclination that I want to add a method on a controller that’s not part of the default five or whatever REST actions that we have by default, make a new controller! And just call it that.
So let’s say you have an
InboxController
and you have anindex
that shows everything that’s in the inbox; and you might have another action where you go like “Oh I wanna see like the pending. Just show me the pending emails in that or something like that”. So you add an action calledpendings
:class InboxesController < ApplicationController def index end def pendings end end
That’s a very common pattern right? And a pattern that I used to follow more.
Now I just go like “no no no”. Have a new controller that’s called
Inboxes::PendingsController
that just has a single method calledindex
:class InboxesController < ApplicationController def index end end
class Inboxes::PendingsController < ApplicationController def index end end
And what I found is that the freedom that gives you is that each controller now has its own scope with its own sets of filters that apply […].
So we’ve had great controller proliferation and especially controller proliferation within namespaces. So let’s say we have a
MessagesController
and below that controller we might have aMessages::DraftsController
and we might have aMessages::TrashesController
and we might have all these other subcontrollers or subresources within the same thing. That’s been a huge success.
So basically he says that controllers should only use the default CRUD actions
index
, show
, new
, edit
, create
, update
, destroy
. Any other action
would lead to the creation of a dedicated controller (which itself only has
default CRUD actions).
What I think about it
I have been happily using this approach for years. The examples he mentions are
only about filtering though, and are probably overkill for simple controller
logic. A common way to filter in REST is by using query parameters (e.g. GET /inboxes?state=pending
), so I would stick to that when the code is short and
simple (once it gets long or complicated or there are too many mixed
actions/concerns, I would do the same as him).
But I totally agree with the general idea of splitting controllers. I like it for several reasons.
It encourages you to produce simpler code
With this technique you can create as many controllers as you want. Use your
judgment though: If the controller only has default CRUD actions and is
relatively short and simple (like the ones scaffolded by Rails), there is
probably no need to prematurely extract each index
/ show
/ etc into their
own controller.
Where the controller splitting technique becomes very cool is when the controller is getting heavier, even when it only has default CRUD actions. What to do then? Well, just put the heavy code in its own dedicated controller!
For example, here is what our most complicated controller looks like at my current company (we are using “relatively skinny models and fat controllers” so YMMV). It allows you to purchase a product in the API part of our app:
class Api::V1::PurchasesController < Api::V1::ApplicationController
rescue_from Stripe::StripeError, with: :log_payment_error
def create
load_product
load_device
load_or_create_user
create_order
create_payment
authorize_payment
confirm_address
render json: @order, status: :created
end
private
def load_product
@product = Product.find_by!(uuid: params[:product_id])
end
# ...
end
It makes your code more uniform
Knowing that there can only be so many CRUD actions in a controller is quite cool. No more guessing/spelunking/endlessly scrolling in long controllers to find that one weird action. No more wondering how/if a custom controller method maps to a route.
I don’t like to be surprised when doing mundane organization. I like uniform code, and heavy convention over configuration is one of the many reasons why I prefer Rails compared to other Ruby frameworks. Everything is organized the same, so you spend less time making mundane decisions, and more time on what really matters for the business.
In theory, it also means that you can move from one codebase to another and be 100% productive in a very short time. In practice there are many non-standard Rails apps in the wild: one company may use architectural patterns such as observers (not my cup of tea), another might use a whole additional architecture layer such as Trailblazer (not my cup of tea either but it has some interesting ideas), another company will use yet another tool, another one will use its own custom sauce, etc.
All of this in part because people seem unhappy with the so-called “lack of structure” in vanilla Rails apps. So they look for additional structure elsewhere. But the solution has been right under our nose all along! Split your controllers, and only use the default CRUD actions. Simple as pie, and junior developer friendly.
Rails could possibly do a better job of promoting this controller splitting technique. Their doc only briefly says “[…] you should usually use resourceful routing […]”. But the idea of CRUD actions and RESTful routes has prominently been featured in their doc for a long time, and if you’ve ever read it, it has probably crossed your mind that adding custom actions (apart from the CRUD ones) is not very “Rails way”. Splitting controllers is a good answer to that uneasy feeling.
It makes you think in terms of REST
A lot of people like REST because it is uniform and simple. Once you understand a (truly) RESTful API, it is easier to understand another one. The business logic obviously differs between applications so you have to understand it, but how you consume that logic is the same: you create a charge in Stripe (i.e. you take someone’s money), you create a text message in Twilio (i.e. you send it), you get a repository in Github, etc.
You have to bend your mind a little at first to use REST nouns instead of actions: you don’t “pay”, you “create a payment”. You don’t “add funds to your balance”, you “create a fund in a balance”. Et cetera. Maybe a bit weird at first, but I would pay this small price any day of the week rather than going back to SOAP, WSDL and all that jazz (ex-Java/JEE developers will know what I’m talking about).
As a bonus, I think that having your whole business logic interface (not necessarily implementation) dictated by REST makes for cleaner and simpler business logic. You can only have objects with so many actions: no more, no less. Yet you know you can express anything with REST, and it will be sound and dependable. It is a liberating constraint.
Here are some example RESTful Rails routes that map to splitted controllers that only use CRUD actions.
resources :purchases, only: :create
resources :costs_calculations, only: :create
namespace :company do
resource :account_details, only: :update
resource :website_details, only: :update
resource :contact_details, only: :update
end
namespace :balance do
resources :funds, only: :create
end
resource :bank_account, only: :update
For best REST design (especially when subresources are involved), I usually
write down the REST action+resource first on a temporary note (example: POST /balance/funds
) without worrying about the implementation. Then when I am
happy with the naming, I translate it to a Rails route, which is easy since
Rails has very good REST support.
Wrapping up
Splitting your Rails controllers when they have a very specific scope, too much logic, or too many mixed concerns can have a lot of good side effects in your code.
The more your app grows, the more time you will need to spend to understand it, no matter how nicely organized the code is. But splitting your controllers makes things easier.