Jonathan Bennett

A better way to handle reusable models

I recently saw a question on Reddit asking how to structure a reusable (polymorphic) model without creating a mess of nearly duplicate routes and controllers:

# config/routes.rb
resources :accounts do
  resources :notes
end

resources :posts do
  resources :notes
end
# ...repeat for every other model with notes

# app/controllers/accounts/notes_controller.rb
# duplicate this for every noteable type
class Accounts::NotesController < ApplicationController
  def create
	@account = Account.find(params[:account_id])
	@note = @account.notes.build(note_params)
	@note.save!

	redirect_to @account
  end

  private

  def note_params
	params.require(:note).permit(:title, :body)
  end
end

They also expressed concern about exposing the related model IDs in the form, since those can be manipulated on the frontend.

A Cleaner Approach with Global IDs

In these cases, I’m a big fan of using a Global ID to identify the noteable object:

puts Post.find(15).to_gid
# gid://MY_APP/Post/15

Even better than a plain Global ID is a signed Global ID. This is a Global ID that’s cryptographically signed on the server, so you can trust it hasn’t been tampered with.

This lets us simplify both our routing and our controller structure:

# config/routes.rb
resources :accounts
resources :notes


# app/models/note.rb
class Note < ApplicationRecord
  belongs_to :noteable, polymorphic: true

  def noteable_sgid
	noteable.to_sgid
  end

  def noteable_sgid=(sgid)
	self.noteable = GlobalID::Locator.locate_signed(sgid)
  end
end

Now, instead of manually nesting routes and duplicating controllers, we can handle all notes through a single controller:

# app/controllers/notes_controller.rb
class NotesController < ApplicationController
  def create
	@note = Note.create!(note_params)
	redirect_to @note.noteable
  end

  private

  def note_params
	params.require(:note).permit(:title, :body, :noteable_sgid)
  end
end

The View: One Form to Rule Them All

We include the noteable_sgid in a hidden field when rendering the form. This is automatically pulled from the note.noteable_sgid:

<%# app/views/accounts/show.html.erb %>
<%= render "notes/form", note: @account.notes.build %>

<%# app/views/notes/_form.html.erb %>
<%# locals: (note:) %>
<%= form_with model: note do |form| %>
  <%= form.hidden_field :noteable_sgid %>

  <%= form.text_field :title %>
  <%= form.text_area :body %>
  <%= form.submit %>
<% end %>

This approach lets you use conventional Rails patterns, reduce duplication, and securely identify your associated records.