Let Rails Help You

Ask questions Research chat →

https://thoughtbot.com/blog/let-rails-help-you · scraped

rails

Attachments

Scraped Content

— 996 words · 2026-05-19 12:38:36 UTC ·

Excerpt

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. If you want to skip the details and just see the code, you can check out this gist. 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). 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 co
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. If you want to skip the details and just see the code, you can check out this gist. 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). 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 to allow any model to be favoritable. The database structure will look like this: ![](https://images.thoughtbot.com/b9tnjz12dvmhexb39xnxd4x6t8e2_image.png) 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. And the migration looks like this: ```plain text 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: ```plain text 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: ```plain text 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. ```plain text 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. Note that a favoritable has many favorite records, because multiple people can favorite the same record in this system. 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 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 version, which is a more secure version that prevents tampering with the ID. ```plain text 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: ```plain text Rails.application.routes.draw do resource :favorite_records, param: :favoritable_sgid, only: %i[create destroy] end ``` 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: ```plain text <%# 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: ```plain text <%= render 'favorite_record_button', favoritable: note %> ``` How we display the favorite records will depend on your app. We could add an index action to our FavoriteRecordsController 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. 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. ## If you enjoyed this post, you might also like: ## About thoughtbot We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us. ![](https://images.prismic.io/thoughtbot-website/Zn234B5LeNNTwm8l_mentoring.jpg?auto=format,compress) One-on-one mentoring with a thoughtbot developer

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation