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:
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:
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
Popular Gems
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:
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:
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:
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:
-
The Bundler homepage: https://bundler.io
-
The Bundler documentation: https://bundler.io/docs.html
-
The Ruby on Rails guides chapter on dependencies: https://guides.rubyonrails.org/