Bundler and Gems

Gems constitute the package management in the world of Ruby.

If you do not have much time, you can skip this chapter for now and come back to it later if you have any specific questions.

If a Ruby developer wants to offer a specific feature or a program (or collection of programs) to other Ruby developers they can create a package. Those packages are called "gems". They can then be installed with the command gem install.

Have a look at https://rubygems.org and https://www.ruby-toolbox.com for an overview of available gems.

Rails itself is a gem, and any Rails project uses a lot of different gems. You as a developer can add further gems. The program bundle (shipped as part of Ruby since Ruby 2.6, and as gem install bundler before that) helps the developer install all these gems in the right version and takes care of transitive dependencies.

The file Gemfile generated by rails new tells Bundler which gems are to be installed:

Gemfile
source "https://rubygems.org"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.3"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Active Model has_secure_password
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
gem "solid_cable"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false

# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false

# Use Active Storage variants
gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"
end

The format is easy to explain: the word gem is followed by the name of the gem and optionally by a version specifier.

For example, the line gem "rails", "8.1.3" stands for "install the gem with the name rails in version 8.1.3 exactly".

With ~> in front of the version number you can tell Bundler that the newest compatible version should be installed. The rule is: ~> only increments the last component. So gem "rails", "~> 8.1.0" will install 8.1.1 once it ships, but not 8.2. For that you’d write gem "rails", "~> 8.1".

You have the option of installing certain gems only in certain environments. To do so you enclose the corresponding lines in a group :name do …​ end block.

Apart from the file Gemfile there is also Gemfile.lock. The exact versions of all installed gems (including transitive dependencies) are recorded there. A small extract:

Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    actioncable (8.1.3)
      actionpack (= 8.1.3)
      activesupport (= 8.1.3)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionpack (8.1.3)
      actionview (= 8.1.3)
      activesupport (= 8.1.3)
      rack (>= 3.1.15)
      [...]

The advantage of Gemfile.lock is that several developers can work on the same Rails project independently and still be sure they are all running the same gem versions. When a specific version is pinned in the Gemfile.lock, Bundler will use exactly that version. This is also crucial for reproducible production deploys.

Only edit Gemfile and never Gemfile.lock by hand. Gemfile.lock is updated automatically when you run bundle install, bundle update or bundle add.

Thanks to this mechanism you can use and develop several Rails projects with different gem version numbers in parallel on the same machine.

Rails 8 uses bin/rails, bin/rake, etc. — these are "binstubs" that Rails installs into every project’s bin/ directory. They make bundle exec unnecessary for the common commands. Use bin/rails from inside the project and Bundler takes care of the rest.
As of Ruby 3.5 a number of stdlib libraries (logger, ostruct, csv, observer, …​) have been "unbundled" into plain gems. If your old project suddenly tells you cannot load such file — logger after a Ruby upgrade, add gem "logger" (or whichever one bit you) to the Gemfile and bundle install fixes it.

bundle add

The easiest way to add a gem to your project is bundle add NAME. It updates Gemfile, Gemfile.lock, and installs the gem in one step:

$ bundle add acts_as_list
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Installing acts_as_list 1.2.5
Writing lockfile to /path/to/Gemfile.lock

Pass --version "~> 1.0" or --group :development for finer control.

bundle update

With bundle update you can update gems to new versions.

$ bundle update rails
  [...]
$ bin/rails -v
Rails 8.1.3

Without arguments bundle update updates every gem within the constraints declared in Gemfile. That is rarely what you want in a real project; prefer bundle update <gem-name> or the more cautious bundle update --conservative <gem-name>.

After every gem update you should first run bin/rails test to make sure that a new gem version does not add any unwanted side effects.

bundle outdated

If you want to know which gems used by your Rails project have a newer version available you can do this via bundle outdated:

$ bundle outdated
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...

Gem      Current  Latest  Requested  Groups
nokogiri 1.18.10  1.18.11 >= 0       default

To update them change the version numbers in Gemfile and run bundle update <gem>.

bundle-audit

Rails 8’s default Gemfile includes the bundler-audit gem. It checks Gemfile.lock against a database of known security vulnerabilities in gems:

$ bundle exec bundle-audit check --update
Updating ruby-advisory-db ...
No vulnerabilities found

Run it regularly (ideally in CI).

bundle exec

bundle exec is required whenever a program such as rake is used in a Rails project and is present in a different version than the rest of the system. The typical error message looks like this:

You have already activated rake 13.3, but your Gemfile requires rake 13.2.2.
Prepending `bundle exec` to your command may solve this.

In that case it helps to invoke the command with a preceding bundle exec:

$ bundle exec rake db:migrate

binstubs

Every Rails project ships with a bin/ directory containing bin/rails, bin/rake, bin/setup, bin/dev, and a handful of others. These are binstubs — tiny wrapper scripts that automatically run the command under Bundler with the versions pinned in Gemfile.lock. So bin/rails db:migrate is equivalent to bundle exec rails db:migrate, just shorter.

If a gem ships an executable and you’d like a binstub in bin/ for it too, you can generate one:

$ bundle binstubs kamal
Creating binstub bin/kamal

On https://www.ruby-toolbox.com and https://rubygems.org you’ll find most of the available gems. The main problem with gems is that many times you have no idea how active the community around a gem is. So you want to check out their homepage and GitHub repository first. It’s a major headache to upgrade a Rails application which depends on neglected gems.

I’d like to show you a couple of gems which are useful for many developers. But please do your due diligence first before you add a gem.

acts_as_list

Let’s create a to-do-list application which displays a couple of to-dos which can be edited by the user. We just need one scaffold for this. We call the model task:

$ rails new to-do-list
  [...]
$ cd to-do-list
$ bin/rails generate scaffold task name completed:boolean
  [...]
$ bin/rails db:migrate
  [...]
$ bin/rails server
Naming is always important within a Rails project. I’ve seen many examples of the to-do-list application where the Task model has a field task too. Don’t do that. If you have an instance variable @task it is cleaner to have @task.name than @task.task, which is just confusing.

A common feature request for any to-do list is to reorder tasks. For that you need some sort of position field on your model. Because this is such a common problem there is a nice, ready-to-go gem for it called acts_as_list. To use it add this line to the Gemfile and run Bundler:

Gemfile
gem "acts_as_list"
$ bundle install

Or the one-liner:

$ bundle add acts_as_list

Then add a position field to the Task model:

$ bin/rails generate migration AddPositionToTask position:integer
  [...]
$ bin/rails db:migrate

If you already have a full database table of tasks you may want to change the migration to something like this which also sets the position field for existing rows:

class AddPositionToTask < ActiveRecord::Migration[8.1]
  def change
    add_column :tasks, :position, :integer
    Task.order(:updated_at).each.with_index(1) do |task, index|
      task.update_column :position, index
    end
  end
end

The last change is to the task model to make it use acts_as_list:

app/models/task.rb
class Task < ApplicationRecord
  acts_as_list
end

For any new row of the tasks table acts_as_list will set the position field automatically. But that is not all. You can use these methods to move the position of a task and reorder the list:

  • task.move_lower

  • task.move_higher

  • task.move_to_bottom

  • task.move_to_top

And you have access to these useful query methods:

  • task.first?

  • task.last?

  • task.in_list?

  • task.not_in_list?

  • task.higher_item

  • task.higher_items

  • task.lower_item

  • task.lower_items

All far from rocket science, but so much easier to use the gem than to reinvent a wheel.

And don’t forget to change the index action in your tasks_controller.rb so the tasks come back in the right order:

app/controllers/tasks_controller.rb
def index
  @tasks = Task.order(:position)
end

Find more information and the complete documentation about acts_as_list at https://github.com/brendon/acts_as_list

Authentication

Most Rails applications need some kind of authentication system. Rails 8 ships with a built-in authentication generator that scaffolds a complete email/password flow (including password reset mailers and session management). We cover it in detail in Authentication. For the majority of small and mid-sized applications the generator is plenty and you won’t need an external gem at all.

If you want features the generator does not cover out of the box — OAuth/SSO (Google, GitHub, …), two-factor authentication, magic links — or if you prefer a turnkey solution, have a look at the authentication category on ruby-toolbox: https://www.ruby-toolbox.com/categories/rails_authentication

The best-known third-party choice is devise (https://github.com/heartcombo/devise). It’s battle-tested and solves 90% of the cases you’d ever need, but it also pulls in a lot of abstraction, so the learning curve is steeper than the built-in generator.

Authorization

Authentication is only half the battle. You also need a system to limit access to special parts of your Rails application to specific users or user groups. That’s authorization.

Again, you can build it yourself — it is not rocket science — but if you are in a hurry head to https://www.ruby-toolbox.com/categories/rails_authorization

Two widely used gems are pundit (policy-object based) and cancancan (ability-based):

Simple Form

Many Rails developers use the simple_form gem (https://github.com/heartcombo/simple_form) to make their life easier. It builds forms in a more concise way than the default scaffold. I found this topic a double-edged sword: I try to stay as vanilla as possible but I see the attractiveness of simple_form, especially for projects that ship a lot of forms.

Further Information on Bundler

The topic Bundler is far more complex than can be described here. If you want to find out more, visit the following resources: