---
title: Let Rails Help You
teaser: Rails gives you the tools to build custom features that still feel native.
tags: rails,ruby
author: Matheus Richard
published_on: 2025-05-27
---

I've recently built a favoriting system for an app. While the code isn't novel,
it reminded me how clean, flexible, and fun Rails can be when you lean into the
framework’s conventions. Here's how I did it - I hope you'll learn something along the way.

<aside class="info">
  <p>If you want to skip the details and just see the code, you can check out
  <a href="https://gist.github.com/MatheusRich/71b34fb8b5d1fc236394873b2910fb33">this gist</a>.</p>
</aside>

## The requirements

My team wanted users to mark records as favorite in the system. This would make those
records handy for quick access. We needed to support several existing models—and
likely more in the future—so it had to be flexible and reusable. Also, multiple
users should be able to favorite the same record (some records are "public", but
we'll skip how that works for this post).

## The solution

So we need a way to mark a record as favorite. Instead of a boolean field on
each favoritable model, I created a separate model to represent the
relationship between a user and a favoritable. That way, any record with an
associated `FavoriteRecord` is considered favorited. While a bit more complex,
this lets us make any model favoritable without writing new migrations. We'll
use a [polymorphic association][polymorphic-association] to allow any model to
be favoritable.

### The models

The database structure will look like this:

![A diagram showing the database structure for a favoriting system. It shows:
A favorite_records table with a owner_id, favoritable_id and favoritable_type columns;
A user table with an id column;
A "favoritable_model" table with an id column;
Both the user and favoritable_model tables have a has_many relationship with the favorite_records table.
](https://images.thoughtbot.com/b9tnjz12dvmhexb39xnxd4x6t8e2_image.png)

And the migration looks like this:

```rb
class CreateFavoriteRecords < ActiveRecord::Migration[7.2]
  def change
    create_table :favorite_records, id: :uuid do |t|
      t.references :owner, foreign_key: { to_table: :users }, null: false, type: :uuid
      t.references :favoritable, polymorphic: true, null: false, type: :uuid

      t.timestamps
    end

    add_index :favorite_records, [:owner_id, :favoritable_type, :favoritable_id], unique: true, name: 'index_favorite_records_uniqueness'
  end
end
```

And here's our humble `FavoriteRecord` model:

```rb
class FavoriteRecord < ApplicationRecord
  belongs_to :owner, class_name: 'User'
  belongs_to :favoritable, polymorphic: true

  # a user can only favorite a record once
  validates :owner, uniqueness: { scope: [:favoritable_type, :favoritable_id] }
end
```

Let's work on the other side of the association now. First, we make users
have favorite records:

```rb
class User < ApplicationRecord
  has_many :favorite_records, foreign_key: :owner_id, dependent: :destroy
end
```

Then, let's make a model favoritable. For this example, I'll use a `Note` model.
Instead of writing this directly in `Note`, I'll create a `Favoritable` concern
so we can share this behavior with other models.

```rb
module Favoritable
  extend ActiveSupport::Concern

  included do
    has_many :favorite_records, as: :favoritable, dependent: :destroy

    scope :favorite, -> { favorited_by(Current.user) }
    scope :not_favorite, -> { where.missing(:favorite_records) }
    scope :favorited_by, ->(owner) {
      joins(:favorite_records)
        .where(favorite_records: { owner: })
    }
  end

  def favorite!
    favorite_records.create!(owner: Current.user)
  end

  def unfavorite!
    favorite_record_for(Current.user)&.destroy
  end

  def favoritable_sgid
    to_sgid(for: :favoritable, expires_in: nil)
  end

  private

  def favorite_record_for(user)
    favorite_records.find { it.owner_id == user.id }
  end
end
```

So now `Note` just includes that concern and gets all that functionality.

<aside class="info">
  <p>Note that a <code>favoritable</code> has many favorite records, because
  multiple people can favorite the same record in this system.</p>
</aside>

### The controller

We can mark notes as favorite now and we have a few helpful scopes. Let's work
on the next level of abstraction: the controller.

We could create a FavoriteNotesController, but since we want to stay
model-agnostic, we’ll go with a generic FavoriteRecordsController. Similarly,
instead of using `model_id` and `model_type` params, we'll use a
`favoritable_sgid` Signed Global ID. This prevents users from messing with the
params and trying to favorite records that they shouldn't be able to.

[Global IDs][gid] are a cool Rails feature that allows you to have a unique
identifier for any record in your system. Exactly what we want. We're using a
[signed][sgid] version, which is a more secure version that prevents tampering
with the ID.

```rb
class FavoriteRecordsController < ApplicationController
  before_action :set_favoritable

  def create
    @favoritable.favorite!

    redirect_back_or_to @favoritable
  end

  def destroy
    @favoritable.unfavorite!

    redirect_back_or_to @favoritable
  end

  private

  def set_favoritable
    @favoritable = GlobalID::Locator.locate_signed(
      params[:favoritable_sgid],
      for: :favoritable
    )
  end
end
```

Signed IDs help us make this controller look just like any other Rails
controller. Our helpers in `Favoritable` take care of the rest. Pretty neat!

And the route will look like this:

```rb
Rails.application.routes.draw do
  resource :favorite_records,
    param: :favoritable_sgid,
    only: %i[create destroy]
end
```

### The view

When rendering each record in an index view, we can add a button to
favorite/unfavorite it. Let's create a partial for this, so we can reuse it in
several models:

```erb
<%# app/views/application/_favorite_record_button.html.erb %>
<% if favoritable.favorited_by? Current.user %>
  <%= button_to 'Unfavorite',
    favorite_records_path(favoritable_sgid: favoritable.favoritable_sgid),
    method: :delete %>
<% else %>
  <%= button_to 'Favorite',
    favorite_records_path(favoritable_sgid: favoritable.favoritable_sgid),
    method: :post %>
<% end %>
```

So in `app/views/notes/_note.html.erb`, we can render this partial:

```erb
<%= render 'favorite_record_button', favoritable: note %>
```

<aside class="info">
  <p>How we display the favorite records will depend on your app. We could add
  an <code>index</code> action to our <code>FavoriteRecordsController</code> to
  list all favorite records for a user, or we could add a "Favorite" tab to the
  index action of each favoritable model, or even just make favorite records be
  displayed at the top of the list. I'll just leave this up to you.
  </p>
</aside>

## The Rails-way makes custom features feel native

I hope this highlighted how Rails (and Ruby!) helps you build flexible code when
you lean into its conventions. We modeled our task into something that looks
like it came out of a Rails generator, so it fits smoothly into the framework.

When you're designing a new feature, think about how you can use Rails
conventions to make it easier to build and maintain. Modeling work into the MVC
pattern makes it easier to build it in Rails, because the framework is optimized
for that.

[polymorphic-association]: https://guides.rubyonrails.org/v8.0/association_basics.html#polymorphic-associations
[gid]: https://github.com/rails/globalid
[sgid]: https://github.com/rails/globalid#signed-global-ids
