I’m currently building an RSS Reader. One of the core functions is being able to bookmark a feed item to save it for later.
Let’s think of some ways we could approach this on the backend:
Item Update Action
class ItemsController < ApplicationController
# Other Actions...
def update
@item = Item.find(params[:id)
@item.update(item_params)
redirect_to @item
end
private
def item_params
params.require(:item).permit(:bookmarked)
end
end
On the face of it, this seems nice. It’s RESTful, we’re using one of Rails’ default controller actions, and just passing in either true
or false
to set the bookmarked
attribute.
The problem is, what happens when I want to start updating other attributes on the item
? e.g. mark as read / unread? Do I use this endpoint too? Or I want to start pinging third-party services. This controller actions could quickly become unwieldy and be responsible for a lot of scenarios.
Custom Controller Actions
I’ve seen this a lot:
resources :items, only: [:index, :show] do
patch :mark_as_bookmarked, on: :member
patch :mark_as_unbookmarked, on: :member
end
The telltale sign that this is a code smell: we’ve got an extra verb in there with ‘mark’. The difficult thing about this is, in isolation, it doesn’t seem too bad. You don’t have to create a new controller and actions are nicely nested in a single file. Great, right?
But you’ll blink and find your Routes swarming with these custom actions that often behave inconsistently from each other.
Here’s my preferred option:
Additional RESTful Routes
resources :items, only: [:index, :show] do
resource :bookmark, only: [:create, :destroy]
end
The mental model for this is that we’re thinking about how what we want to do fits within the framework of REST. In this case we want to “create a bookmark” or “destroy a bookmark”.
class BookmarksController < ApplicationController
def create
@item = Item.find(params[:item_id])
@item.update(bookmarked: true)
redirect_to @item
end
def destroy
@item = Item.find(params[:item_id])
@item.update(bookmarked: false)
redirect_to @item
end
end
Here’s why I like it:
- We’re staying within Rails’ RESTful approach and not having to make decisions on naming actions.
- We’re not overloading our existing controllers with new actions.
- As the app grows, we have a better environment to manage additional functionality (like pushing bookmarks to a third party service).
Your controllers don’t have to map 1:1 to your active record models (see: The Map is Not The Territory).