This post is with apologies to former-thoughtbottle Derek Prior, whose talk In Relentless Pursuit of Rest answered some crucial questions for me recently.
A note about this post’s title: I refer to “some parts of REST” because RESTful architecture includes many ideas which are not covered in any depth (or at all) in Derek’s talk. The focus on routing and controllers is due to the popularity and frequency of debate over these concepts (as opposed to e.g. discoverability from a root URL) in our corner of the web development world.
It’s fairly common to see a Rails app that has been around for some time with lots of logic in really complicated and long controllers. This is bad, according to some of the earliest advice on writing Rails apps. We should aim for small controllers and put business logic elsewhere.
A related issue is extensive use of custom routes, which the Rails guide on routing discourages: “you should usually use resourceful routing”.
Nonetheless: there are plenty of Rails apps which generate good revenue that ignore this advice. So if – as a consultant – I were to issue the same advice, I had better know why it’s worth doing something different when “it ain’t broke”.
Here’s why we need small controllers and why RESTful routing helps, with a hat tip again to Derek Prior:
First, large controllers have all the disadvantages of other large source code files: potential for highly coupled code that is scattered so far apart you can’t even see it on the same screen, and full of complex conditionals that hide bugs.
Second, controllers attract conditionals because you often end up with pseudo-DRYness where a piece of code seems identical apart from “this one tiny difference”.
Third, controllers are typically not well tested. The Rails testing community has evolved in an environment that assumes that controllers are really simple, so you can’t test them in isolation by default. If you want to have one test per branch, you have to set up a request spec that is also considering routing and views for each branch. I’m not saying that Rails needs to bring back controller testing; I’m saying that controllers should be simple enough that you don’t need a lot of request specs for each one.
Fourth, the custom routes that point URLs to the right action are usually ambiguous and inconsistent. They follow patterns like:
resource :users do post "edit-password" post "add-payment-method" post "activate" ...
Looking at the sort of URL generated here, like
/users/edit-password, there’s a pattern of
noun/verb-noun. The verb part of this usually provides no insight into what is actually being done, and is a point of inconsistency. Future developers have to choose:
add-, or what about
What about this alternative:
resources :users do member do resource :password, only: [:update] resource :trial, only: [:create, :update] resources :payment_methods, only: [:create, :destroy] ...
Now, there is no confusion over which verb to use. The verbs are constrained to the standard Rails verbs which map to standard HTTP methods, and are consistent across all your routes. I also switched the verb “activate” to the noun “trial”, because the process of moving away from verbs made me ask: “what does ‘activate’ actually do?”
Further, these routes allow us to move towards smaller controllers, splitting one lengthy, complex
UsersController into several much smaller and easier to read files.
It’s worth considering some objections here:
- All of these controllers are still backed by one database table. There isn’t actually any reason why controllers and models and database tables should be 1:1:1. And in fact this proliferation of routes could lead you to decide that you want to split that god-object User model into several models and tables.
- Every controller will need to duplicate at least
User.find(...). We do have some duplication, but of really simple code. Chances are that splitting a big controller up does not create duplication so much as reveal duplication that was hidden in tightly coupled private methods. Developers have many tools (especially composition) to help with duplication. Bear in mind also that duplication is better than the wrong abstraction.
- It’s so much easier to copy paste an action method than to create a whole new controller. I think this goes away with experience; and seems to place emphasis on the cost of writing code over the experience of reading it. Code is read many more times than it is written.
- That’s so many different files to refer between. We might get the split wrong, and find ourselves referring to multiple files in order to achieve a single outcome. At least whatever we change will be a small piece of code. I personally find it easier to work with split windows showing two different files than I do referring to two windows on the same file. And “find in project” tooling is pretty good; is the problem really that you aren’t comfortable navigating files in your projects?
- You’re asking me to change my beautiful URLs. If you want vanity URLs, you can also add those into your routes file and have them redirect to your resourceful routes. You don’t even need a separate controller to do it, you can stay in the routes file.
I’ve found it helpful for me to distinguish between the arguments for resource-based routing and small controllers that are important, and those that don’t make much difference to me. The latter includes:
- don’t fight the framework. I know people (including colleagues) who feel strongly about this, I’m just not one of them; people deviate from Rails when they need to. Custom routes are allowed for in the routing DSL, so it’s clearly not forbidden by the framework (albeit discouraged).
- you can generate more code automatically with resources. I’ve never found that sheer typing speed is a major factor in the success of my projects, so I don’t care about automation beyond the benefit of consistency.
- onboarding new developers will be easier if you follow the Rails conventions. This assumes that every new developer is familiar with Rails, but many employers are happy to hire people from elsewhere.
I don’t want to get caught up in back-and-forth over points like these that don’t relate to the real benefits I want, so I won’t raise them.
I’m pleased to have some real ammunition to talk to clients about why small controllers and RESTful routing is good:
- big files are hard to parse and prone to hidden coupling
- controllers are often poorly scrutinised for bugs
- the verbs included in custom routes are an additional decision point for little value
- the nouns that REST points us towards are less ambiguous than the verbs commonly used in custom routes
Thanks to Derek Prior for explaining all of this in his talk so long ago, and thanks to my colleague Mattheus who finally inspired me to choose to watch it out of all my other bookmarks. It’s worth a watch!