Define Custom Routes Using the Member and Collection Blocks in Rails

Define Custom Routes Using the Member and Collection Blocks in Rails

October 15, 2022

Let's say you want to add non-resourceful custom routes on your controller. Most often, you're better off by introducing a new resourceful controller. However, in those cases where you absolutely have to, you can define new routes using the member and collection blocks provided by the Rails router.

RESTful routes in Rails provide you with a nice set of named routes which are mapped to the common, standardized URL patterns. However, there might be situations when you want to veer of the beaten path and create custom routes in addition to the RESTful ones. Rails lets you accomplish this using member and collection routes. Let's learn how they work.

But first, do you really need them at all?

Before you continue, keep in mind that a better alternative to adding custom, non-resourceful routes is to introduce a new resourceful controller. Not only this keeps your code clean, but gives you better domain model.

Here's what David has to say on this:

What I’ve come to embrace is that being almost fundamentalistic about when I create a new controller to stay adherent to REST has served me better every single time. Every single time I’ve regretted the state of my controllers, it’s been because I’ve had too few of them. I’ve been trying to overload things too heavily.

David Heinemeier Hansson, Full Stack Radio

That said, reality is often messy, and Rails realizes this fact, providing you two ways to add custom, non-resourceful routes for your application.


TL;DR

When you need to add more actions to a RESTful resource use member and collection routes.

# bad
get 'articles/:id/preview'
resources :articles

# good
resources :articles do
get 'preview', on: :member
end

# bad
get 'posts/search'
resources :posts

# good
resources :posts do
get 'search', on: :collection
end

If you want to create more than one member/collection routes, use the alternative block syntax.

resources :articles do
member do
get 'preview'
get 'search'
# more routes
end
end

resources :images do
collection do
get 'search'
get 'preview'
# more routes
end
end

Again, do you really need these custom routes at all? Could you introduce SearchController and PreviewController with resourceful actions such as indexshow, and create instead?

Anyway, let's dig in to learn how you can create custom routes when you absolutely have to.


A single call to resources in the routes.rb file declares the seven standard routes for your resource. What if you need additional routes? Don't worry. Rails provides the member and collection blocks so you can define custom routes for both the resource collection and the individual resource.

Here's how you'd typically define routes for the article resource.

resources :articles

This creates the following routes.

➜ bin/rails routes -g article

Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy

But let's say you are writing your articles in markdown, and need to see a preview of the article as you write it.

You could create a PreviewController and display the article's preview using its show action, but it's convenient to add a preview action on the ArticlesController itself.

Custom Member Routes

Here's how you define the preview route on the ArticlesController using the member block.

resources :articles do
member do
get 'preview'
end
end

The Rails router adds a new route that directs the request to ArticlesController#preview action. The remaining routes remain unchanged. It also passes the article id in params[:id] and creates the preview_article_path and preview_article_url helpers.

➜ bin/rails routes -g article

Prefix Verb URI Pattern Controller#Action
preview_article GET /articles/:id/preview(.:format) articles#preview

...remaining routes

If you have a single member route, use the short-hand version by passing the :on option to the route, eliminating the block.

resources :articles do
get 'preview', on: :member
end

You can go one step further and leave out the :on option.

resources :articles do
get 'preview'
end

It generates the following route.

➜  bin/rails routes -g preview

Prefix Verb URI Pattern Controller#Action
article_preview GET /articles/:article_id/preview(.:format) articles#preview

There are two important differences here:

  1. The article's id is available as params[:article_id] instead of params[:id].
  2. The route helpers changes from preview_article_path to article_preview_path and preview_article_url to article_preview_url.

Custom Collection Routes

To add a new route for the collection of a resource, use the collection block.

resources :articles do
collection do
get 'search'
end
end

This adds the following new route. It will also add a search_articles_path and search_articles_url helper.

search_articles GET    /articles/search(.:format)   articles#search

If you don't need multiple collection routes, just pass :on option to the route.

resources :articles do
get 'search', on: :collection
end

This will add the same route as above.

Conclusion

Rails allows you to break out of its convention of using seven resourceful routes using the member and collection blocks. Both allow you to define additional routes for your resources than the standard seven routes. A member block acts on a single member of the resource, whereas a collection operates on a collection of that resource.

Sign up for my newsletter

Let's learn to become better developers.

Comments

No comments yet. Be the first to leave one.

Sign in to leave a comment.