Stefan Vermaas |
Ruby on Rails advocates heavily for "convention over configuration". One of these core conventions is the baked-in RESTful design for CRUD actions. You're introduced to this convention when you're just starting out with Ruby on Rails.
Almost all web applications involve CRUD (Create, Read, Update, and Delete) operations. You may even find that the majority of the work your application does is CRUD. Rails acknowledges this and provides many features to help simplify code doing CRUD.
- The Rails docs
There is one big disadvantage about "convention over configuration"; it's not always obvious what principle is applied, why it's being applied, and how it should be applied when things get harder.
The centerpiece of the REST principle is a resource. Interacting with a resource should be done with stateless HTTP requests using one of the four verbs: GET, POST, PUT/PATCH, or DELETE.
With "stateless HTTP requests," we mean that the server should be able to perform the operation with the given information from the client. This information can be as part of the URI, query-string parameters, body, or headers.
In Rails, a less pure implementation of REST is often used because most Rails apps are both the client and the server.
Uniform Resource Naming (URN) of resources is another part of REST. The name of the resources should reflect what it is: a singleton or a collection.
A list of repositories on Github is a collection resource that can be identified with the
/repositories URN. An individual repository is a singleton resource and can be identified with the
Want to learn more about REST? See "Architectural Styles and the Design of Network-based Software Architectures", the dissertation of Roy Fielding.
In Rails, a resource is often translated to an ActiveRecord model. However, an ActiveRecord isn't the only resource in a Rails app. A resource can be anything according to the REST design principles.
It can be an ActiveRecord model, it can be a Plain Old Ruby Object (PORO) model, it can be a service, an operation, and so on.
As we mentioned before, Rails is opinionated. Resourceful routing is one of these conventions. In the Rails docs, the preference for using resourceful routes is briefly mentioned:
While you should usually use resourceful routing...
Ruby on Rails provides a resourceful way to map the HTTP requests and URLs to controller actions. There are multiple ways to define this mapping. However, the main two ways are to define a route;
Let's take a look at a simple example that uses
# config/routes.rb Rails.application.routes.draw do resources :repositories, only: %i[index show] do resources :collaborators, only: %i[index] end end
With the routes above, we're able to show a list of repositories and for each of these repositories, we can show a list of collaborators. Let's see how non-restful routes can make sense at first, but start to be a problem further down the road.
Let's say we want to invite a new collaborator to a repository by extending our previous example with non-RESTful routes.
# config/routes.rb Rails.application.routes.draw do resources :repositories, only: %i[index show] do resources :collaborators, only: %i[index] do post :invite end end end
Seems reasonable, doesn't it? We're inviting a new collaborator to a repository. However, now the new collaborator needs to be able to accept and decline the invite. So we add it as a custom action to the controller too.
# config/routes.rb Rails.application.routes.draw do resources :repositories, only: %i[index show] do resources :collaborators, only: %i[index] do post :accept_invite post :decline_invite post :invite get :show_invite end end end
This approach still seems to be reasonable, however, our
collaborators_controller.rb starts to grow. It's not only responsible anymore for listing the collaborators, but also for handling the invites.
It's not only breaking the Single Responsibility Principle (SRP) that goes hand-in-hand with a RESTful API design, but it will also make it harder to maintain the codebase as it grows. The
collaborators_controller.rb will become longer, harder to read, and thus harder to maintain.
Let's see how the above example could be improved if one would follow a RESTful API design.
If we're following the REST design principles, we will see that we're not actually inviting a new collaborator. We're creating an invitation for the collaborator which they can accept or decline.
REST encourages us to think in terms of creating/updating/deleting resources rather than in actions. So instead of inviting a collaborator, we create an invitation for a collaborator. The same is true regarding accepting or declining an invitation.
Wrapping your head around REST can take some time but it will make your code much cleaner and easier to maintain. Let's take a look at a more RESTful approach to inviting new collaborators.
# config/routes.rb Rails.application.routes.draw do resources :repositories, only: %i[index show] do resources :collaborators, only: %i[index] resources :invitations, only: %i[show create update destroy] end end
Now, we enable collaborators to see an invitation for a repository before accepting/declining it. We also allow them to accept the invitation by updating it and we allow them to decline the invitation by deleting it.
By expressing our intentions in a RESTful way, we're separating the responsibilities of our controllers and are keeping them small. This will not only make it easier to maintain and to read through them, but it will also make them easier to test.
Following the REST design principles will help you to keep responsibilities of a controller to a minimum. Whenever a new feature request comes in, ask yourself "how can I make this RESTful?"
Splitting up your controllers, following the REST design principles, and reducing the responsibilities of a single controller will help you to make your applications more maintainable.