I wanted to share a little something cute I noticed a couple of weeks ago while adding a new feature to an old Rails application. It gave me a new appreciation for the attention to detail that the authors of the Rails framework put into making it a pleasant development experience.
For the sake of demonstration, let’s pretend I was adding a new endpoint for a user to add an avatar to their account, and for other users to view that avatar. Due to other imaginary implementation details, we use a third-party service for handling avatars, and not a few extra fields on the existing users
table in the database backing our app. We just need to integrate with that made-up service. An avatar could be server-rendered HTML, or an API that returns JSON–again, doesn’t matter for this story.
The application is legacy, by which I mean, someone besides me wrote it. So I don’t have full context behind all the decisions behind its design and architecture. But let’s say there’s an existing UsersController
, and it contains familiar Create/Read/Update/Delete (CRUD) methods like new
, show
, create
, and update
, specific to records in the database’s users
table. But it also includes non-standard methods like add_permission
, and links
. What these methods do isn’t important, just that they’re outside the typical scope of a single Rails controller as a web front-end for a single resource’s CRUD actions.
I could follow this pattern as established in the existing code, and include my avatar-related functionality on this controller, and update my routes configuration accordingly:
Looking at the routes alone, that doesn’t seem too bad, I guess. But things start to get gnarly when writing the controller code. Pretty quickly, we start to see the controller’s responsibility for resources outside of the User
model grow. And with that responsibility comes controller complexity. And with that complexity comes difficulty testing and maintaining.
If I looked at the route helpers generated by Rails for these new actions (using the rails routes
command line tool), I might’ve sooner gotten the sense that I was heading down a wrong path:
Helper | HTTP method | Path | Controller/action |
---|---|---|---|
user_avatar |
GET |
/users/:user_id/avatar(.:format) |
users#avatar |
user_new_avatar |
GET |
/users/:user_id/new_avatar(.:format) |
users#new_avatar |
user_save_avatar |
POST |
/users/:user_id/save_avatar(.:format) |
users#save_avatar |
user_avatar_path
is fine. user_new_avatar_path
is sort of fine, maybe. But user_save_avatar_path
is gross. It just doesn’t read well. It’s like I’m saying, Hey user, save avatar! and not Hey Rails program, save this user’s avatar! even though the User
model has zero responsibility for managing avatar data. It’s almost like Rails is telling me not to continue down this rabbit hole. So I won’t. What about a nested resource, instead?
For this example, I’m limiting this to just the show
, new
, and create
actions to correspond with the functionality from the first approach. But there’s no reason this couldn’t handle editing/updating or deleting avatars, too.
With the nested resource in place in my routes, here are the corresponding helpers:
Helper | HTTP method | Path | Controller/action |
---|---|---|---|
new_user_avatar |
GET |
/users/:user_id/avatar/new(.:format) |
avatars#new |
user_avatar |
GET |
/users/:user_id/avatar(.:format) |
avatars#show |
user_avatar |
POST |
/users/:user_id/avatar(.:format) |
avatars#create |
It’s subtle, but new_user_avatar_path
reads more like plain English than the corresponding user_new_avatar_path
, doesn’t it? And a POST to user_avatar
now follows Rails convention, expecting a create
method on AvatarsController
. I still have to write that AvatarsController
class with a create
method, but the rest of the plumbing is done for me.
Grammar matters, and in this case, thinking about code from a human perspective, and not a computer perspective, pointed me toward a better design with smaller, less complicated controllers. Smaller controllers are easier to maintain, and easier to test. And by following the Rails convention of limiting controllers to the seven CRUD actions, I’ve got a clean template for adding my new functionality.
I know that full appreciation of this feature of Rails assumes proficiency in English grammar. But Rails and Ruby have pretty much always leaned into this expectation as a means to reach the goal of programmer happiness. And programmer happiness is what keeps me reaching for Ruby on Rails, 15 years in.
By the way, if you’re not sure about this in your own code, you can experiment with routes and route helpers before writing any corresponding controller code. This is a great way to confirm whether you’re on the right track with your design. And if you haven’t before, I strongly recommend reading Rails Routing from the Outside In for a more thorough overview of routing in Rails, to help understand all the different ways you might structure your app’s HTTP entry points for programmer happiness.
Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.