Last Update: July 6, 2017

Make Rails Crud Again

Routes in Ruby on Rails should rarely not be RESTful. Also, alliteration is always awesome!

REST is an acronym for Representational State Transfer. Without getting too academic, it refers to using URLs to identify resources (e.g. /products/42) and HTTP methods (e.g. POST) to identify actions. In a RESTful system, one would expect DELETE /products/42 to delete the product with unique identifier 42.

By adhering to the convenient Rails routing conventions, we can easily setup RESTful routes to handle the typical CRUD (i.e. create, read, update, delete) actions for our resources.

# config/routes.rb
resources :products

Now, let’s run $ rails routes.

$ rails routes
      Prefix Verb   URI Pattern                  Controller#Action
    products GET    /products(.:format)          products#index
         POST   /products(.:format)          products#create
 new_product GET    /products/new(.:format)      products#new
edit_product GET    /products/:id/edit(.:format) products#edit
     product GET    /products/:id(.:format)      products#show
         PATCH  /products/:id(.:format)      products#update
         PUT    /products/:id(.:format)      products#update
         DELETE /products/:id(.:format)      products#destroy

This reveals the URLs and controller actions to which they map. A GET request to /products/new triggers the new action in the products controller. A PUT or PATCH request to /products/:id triggers the update action in the products controller. You get the picture.

If we want to omit a specific action like deleting a product, we can tell Rails to omit that action:

# config/routes.rb
resources :products, except: [:destroy]
$ rails routes
      Prefix Verb   URI Pattern                  Controller#Action
    products GET    /products(.:format)          products#index
         POST   /products(.:format)          products#create
 new_product GET    /products/new(.:format)      products#new
edit_product GET    /products/:id/edit(.:format) products#edit
     product GET    /products/:id(.:format)      products#show
         PATCH  /products/:id(.:format)      products#update
         PUT    /products/:id(.:format)      products#update

Using RESTful routes and CRUD actions across all controllers gives our apps consistency and makes the controller level easy for other developers to understand. It also helps keep the controllers slim and easy to manage.

New Requirement

What happens when we encounter an action that doesn’t quite fit the CRUD acronym? Suppose our client wants to be able to archive a product. On one hand, archiving can be as simple as adding an “Archived?” checkbox to the product form. On the other hand, if the app needs to send out email notifications or run another background job when a product is archived, it will be much cleaner to separate archiving into it’s own route/action.

Rails provides the abilty to extend resource routes beyong the typical CRUD actions via the member method.

# config/routes.rb

resources :products do
  member do
    post 'archive'
  end
end

This adds a new URL that maps to the archive action of our products controller.

archive_product POST   /products/:id/archive(.:format) products#archive

Assuming we setup our products controller and coded the archive method, we can now add a link in our views to archive a product:

<%= link_to 'Archive', archive_product_path(@product), method: :post %>

CRUDA+?

We may be tempted to use this approach when adding non-CRUD actions. After all, it’s just one extra method, right? Instead of CRUD, our app is CRUDA (A for archive)!

What happens when we need to add the ability to un-archive a product? We can just add an unarchive member route and corresponding action, right? Now, we have CRUDAU. Or, maybe to keep it short, CRUDA+!

Now, things are getting confusing. Our products controller has two extra methods, and our nice little CRUD acronym no longer holds up. A developer looking at our code might not understand how the archive and unarchive actions are used, and our products controller is gaining new responsibilities which will lead to new reasons to change in the future.

A Metaphor

Think of the LGBT community. LGBT is an abbreviation that represents individuals who are marginalized based on sexual preference or gender identity. It’s powerful because it’s simple, memorable, and it covers groups that are arguably fighting the same battle.

Eventually, due to many individuals feeling like the LGBT abbreviation doesn’t accurately describe their identities, LGBT was expanded to LGBTQ. When that wasn’t enough, abbreviations like LGBTQ+, LGBTQQ, LGBTI, and even LGBTTTIQQA arose.

I respect everyone as an individual, and I am supportive of marginalized groups fighting for equal rights. However, it’s completely understandable for outsiders to be confused by and even dismissive of a group that gives itself a label as confusing as LGBTTTIQQA.

A Different Way of Thinking

If we want our controllers to remain under the CRUD acronym, we need to find a new home for the code that archives a product. We need to change the way we think of archiving so that we’re creating, reading, updating, or deleting something.

If you’ve ever used the Devise gem for user authentication, you’ve seen an example of this. It may be intuitive and tempting to place the action for logging a user into the app in a login method inside the users controller. However, Devise says “We’re not logging in a user. We’re creating a session!” So, the code for logging a user into the app belongs in the create method of a sessions controller.

There are multiple ways to think about the action of archiving a product. Here’s one way: instead of archiving a product, let’s say we’re creating an archive.

resources :products do
  resource :archive, only: [:create]
end
$ rails routes | grep archive
product_archive POST   /products/:product_id/archive(.:format) archives#create

Now, the URL for archiving a product maps to the create action of the archives controller. We can still include an archive link in our views.

<%= link_to 'Archive', product_archive_path(@product), method: :post %>

Our controllers are both RESTful and limited to CRUD responsibilies.

Make Rails Crud Again! Buy the hat!