Resourceful Routes
Resourceful routing allows you to quickly declare all of the common routes for a given resource.
The concept of resources is very powerful in Rails. With a single call to a method named resources
, Rails will generate seven different routes for index
, show
, new
, create
, edit
, update
, and destroy
actions, saving you a lot of typing.
But saving a few keystrokes is just the cherry on top. The biggest benefit of using resourceful routing is that it provides a nice organizational structure for your Rails application, and helps you figure out how to name and organize the controller classes and action methods.
Not only that, resourceful routing imposes certain constraints on your project which makes most Rails applications consistent and familiar. When a developer says that they have a post resource, another can safely assume (most of the time) that they have a PostController
class with actions like create, show, edit, new, ...
along with a Post
model saved in the posts
table in the database. This conceptual compression is not to be taken lightly.
Since the concepts of REST and Resource are such an integral part of all Rails applications, let's try to understand them in depth.
What's a Resource?
A resource, like an object, is one of those concepts that's difficult to put into words. You intuitively understand them, but you can't express them.
I'll stick to how Roy Fielding, the inventor of REST, defined them in his dissertation.
A resource R is a temporally varying membership function Mr(t), which for time t maps to a set of entities, or values, which are equivalent.
Is your head spinning? 🤯 Mine too. Let's try the other definition he gives in the same section.
A resource is the key abstraction of information. Any information that can be named can be a resource.
Much better, right? 💡 I think that definition captures the essence of resources really well.
Any concept in your application domain can be a resource: an article, a course, or an image. It doesn't have to be a physical entity, but can be an abstract concept such as a weather forecast or an SSH connection to connect to a remote server.
The main point is: you have total freedom in choosing whatever idea you are using in your application as a resource.
How do Resources Relate to REST?
Now, you may have heard the term REST (Representational State Transfer). In simple terms, REST is a way for different machines on the Internet to communicate with each other using resources.
It's important to remember that you only get a representation of the resource (hence the name 'Representational State Transfer').
For example, if you ask the HTML representation of a Post resource (like Hotwire or traditional web apps), you'll get the post as an HTML. If you ask for a JSON representation (like many SPA frameworks do), you'll get the post JSON. Makes sense?
But Why do I need a Resource?
Good question. You might be wondering what this discussion of resources has to do with routing. Well, resources are fundamental to routing. They allow you to reduce seven different routes into a single method call.
Let's learn how.
Identifying Patterns
If you've used any software application, you must have noticed that most of them have a few common patterns. Most of the time, you're:
- Creating a resource (publishing a post, uploading an image)
- Reading that resource (viewing the tweets, listening to podcasts)
- Updating the resource (changing source code on GitHub, editing the post)
- Deleting the resource (removing a folder from Dropbox, deleting your profile picture)
No matter how fancy your application is, I guarantee it will have some form of these four actions that your users can take. Collectively, they're called CRUD which stands for Create, Read, Update, and Delete.
A resource is an object that you want users to be able to access via URI and perform CRUD operations.
For each of these actions, your application should have a route. Assuming you're building a course platform, your routes file might have the following routes.
post 'courses' => 'courses#create'
get 'course' => 'course#show'
patch 'courses/:id' => 'courses#update'
delete 'courses/:id' => 'courses#destroy'
In addition to these four routes, you'll typically have three more:
- one route to see all the courses
get 'courses' => 'courses#index'
- two routes to fetch the pages that allow the users create and modify the courses. These are different from the routes that actually save and update the courses.
get 'courses/new' => 'courses#new'
get 'courses/:id/edit' => 'courses#edit'
So we have a total of seven routes that are common to most resources in most web applications.
- If you are building a blog, you'll have these seven routes for posts.
- If you're building Instagram, you'll have the same routes for images.
- If you're building a course platform, you'll have these routes for courses and lessons.
No matter the topic, you'll most likely have some subset of these seven routes for each resource.
Since this pattern is so common and repetitive, Rails introduced the concept of resourceful routing, whereby calling one method named resources
and passing the plural name of the resource, i.e. :posts
, :courses
, :images
, etc. you get these seven routes for free. Rails will automatically generate them for you.
Rails.application.routes.draw do
resources :courses
resources :photos
resources :posts
end
In addition, resourceful routing also generates sensible names for these routes. For example, you'll have names like course_path
, edit_course_path
at your disposal without providing the :as
option.
The following table summarizes what you get with a single call to resources :courses
.
➜ bin/rails routes -g course
Prefix Verb URI Pattern Controller#Action
courses GET /courses(.:format) courses#index
POST /courses(.:format) courses#create
new_course GET /courses/new(.:format) courses#new
edit_course GET /courses/:id/edit(.:format) courses#edit
course GET /courses/:id(.:format) courses#show
PATCH /courses/:id(.:format) courses#update
PUT /courses/:id(.:format) courses#update
DELETE /courses/:id(.:format) courses#destroy
The values under the Prefix column represent the prefix of the route's name, e.g. new_course
gives you new_course_path
and new_course_url
helpers, and so on. The rows with empty prefixes just follow the ones from the above row.
Resourceful routing allows you to quickly declare all of the common routes for a given resource. A single call to resources declares seven routes for index, show, new, create, edit, update, and destroy actions.
In addition to having multiple resources, there's also a singular form of resourceful routes. It represents a resource that only has one, single form. For example, a logged-in user's profile. You can use the resource
method for this.
resource :profile
It creates the following routes:
new_profile GET /profile/new(.:format) profiles#new
edit_profile GET /profile/edit(.:format) profiles#edit
profile GET /profile(.:format) profiles#show
PATCH /profile(.:format) profiles#update
PUT /profile(.:format) profiles#update
DELETE /profile(.:format) profiles#destroy
POST /profile(.:format) profiles#create
Note two important differences from the plural version:
- The route to show all profiles is missing. Since we only have a single profile, there's no point in displaying all.
- None of the routes contain an
id
segment key. We don't need it to identify a profile as there's only one profile.
What if I don't have all seven routes?
Sometimes, you don't want all seven routes that resources
method creates for you. In such cases, you can pass the only
or except
options to filter the ones you need or don't need.
resources :courses, only: [:index, :show]
resources :courses, except: [:delete]
You can even nest multiple resources, but I'll leave that topic for a different post.
Can I define more routes in addition to the seven resourceful routes?
Yes, Rails provides you the member
and collection
helpers to define non-conventional routes. We'll explore the topic of custom routes in the next lesson.
That said, any time you find yourself reaching for non-resourceful routes, stop and consider if you need a separate resource with a dedicated controller instead.
How to Map a Resourceful Route in Rails to Another Controller Class
You can map a resource to a different controller by passing the controller
option to the resources
method.
resources :posts, controller: "articles"
Without the controller
option, the posts
resource route will map to the PostsController
class.
With the controller option provided, it will map to the ArticlesController
class.
$ bin/rails routes -g posts
Prefix Verb URI Pattern Controller#Action
posts GET /posts(.:format) articles#index
POST /posts(.:format) articles#create
new_post GET /posts/new(.:format) articles#new
edit_post GET /posts/:id/edit(.:format) articles#edit
post GET /posts/:id(.:format) articles#show
PATCH /posts/:id(.:format) articles#update
PUT /posts/:id(.:format) articles#update
DELETE /posts/:id(.:format) articles#destroy
Conclusion
To summarize what we've learned so far, any concept in your application domain can be a resource: an article, a course, or an image, even a user session. It doesn't have to be a physical entity, but anything that can be named.
Resourceful routing allows you to quickly declare all of the common routes for a given resource. A single call to resources
declares seven routes for index
, show
, new
, create
, edit
, update
, and destroy
actions.
Using resourceful routing not only structures your Rails codebase but also provides a nice organizational structure, answering the common questions: what to name this controller or method. So put it to good use, and only resort to non-resourceful routes when you have to.
As we saw, the Rails router is very powerful and highly flexible. Routing is also a two-way street: match URLs to controller actions, and generate URLs dynamically. Additionally, the router uses powerful conventions such as resourceful routing to automatically generate sensible routes for you.
If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. You can also subscribe to my blog to receive future posts via email.