Unnesting routes in Rails

Written . Tagged Ruby on Rails.

If your app has sellers with contracts, you may reasonably end up with paths like /sellers/1/contracts.

After all, you need the seller id to know what subset of contracts to list.

If contracts then have items, you may find yourself with paths like /sellers/1/contracts/2/items/3/edit.

That’s a mouthful, and you need to provide all those ids every time you generate a path: edit_seller_contract_item_path(seller, contract, item)

Instead, let’s rethink it.

Sure, you need the seller id to know what subset of contracts to list. So /sellers/1/contracts is fine.

But when you show a specific contract, the seller id is redundant. Therefore /contracts/2 is enough.

What about the other actions?

Whenever you have the contract id, the seller id is redundant. So that applies to the show, edit, update and destroy actions.

Only index, new and create require the seller id, because the contract doesn’t yet exist.

This all works really well in Rails. Just route them separately:

config/routes.rb
1
2
3
4
5
resources :contracts, only: [ :show, :edit, :update, :destroy ]

resources :sellers, only: [ :index, :show ] do
  resources :contracts, only: [ :index, :new, :create ]
end

Of course, you only list the actions you use. If you have many routes, you want to do this anyway, for speed.

You can even specify shallow: true and Rails will do this for you:

config/routes.rb
1
2
3
resources :sellers, only: [ :index, :show ] do
  resources :contracts, only: [ :index, :show,  ], shallow: true
end

Rails is clever enough that all the contract routes will go to one and the same controller with no extra effort:

app/controllers/contracts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
class ContractsController < ApplicationController
  def index
    @seller = Seller.find(params[:seller_id])
    @contracts = @seller.contracts
  end

  def show
    @contract = Contract.find(params[:id])
  end

  # …
end

Because you typically nest routes under show – and rarely under index, new or create – this ensures that you’ll rarely have more than two levels of nesting, even if the chain of records is long.

So even if your sellers have contracts with items with viewings, the routes at the far end of that graph will be no longer than /viewings/4, instead of /sellers/1/contracts/2/items/3/viewings/4.