Skip to content

09 · Gems & Bundler Basics

A gem is a Ruby package — a library someone else wrote that you can install and use in your own code. Bundler manages exactly which gems (and versions) a project depends on.

Installing a gem directly

gem install colorize
require "colorize"

puts "Success!".green
puts "Warning!".yellow
puts "Error!".red

require loads a library — either from Ruby's standard library (like json or date) or from an installed gem.

Why Bundler exists

Installing gems globally with gem install works for quick experiments, but real projects need reproducible dependencies — the exact same gem versions on every machine that runs the code. Bundler solves this with a Gemfile and a Gemfile.lock.

Gemfile

# Gemfile
source "https://rubygems.org"

gem "colorize"
gem "httparty", "~> 0.21"     # pessimistic version constraint
gem "rspec", group: :test       # only installed in the :test group
bundle install

Running bundle install reads the Gemfile, resolves compatible versions for every gem, installs them, and writes the exact resolved versions to Gemfile.lock — that lock file is what guarantees everyone on the team (and your production server) gets identical versions.

Version constraint syntax

Syntax Meaning
gem "foo" Any version
gem "foo", "1.2.3" Exactly this version
gem "foo", ">= 1.2" This version or newer
gem "foo", "~> 1.2" >= 1.2, < 2.0 (pessimistic — allows patch/minor updates only)

~> ("twiddle-wakka") is the most common in practice — it allows safe patch and minor updates while protecting against unexpected breaking major-version bumps.

Running code with Bundler

bundle exec ruby my_script.rb

bundle exec ensures your script uses the exact gem versions locked in Gemfile.lock, rather than whatever happens to be installed globally on the system — always prefer this over a bare ruby command once a Gemfile exists.

Requiring gems in your code

# my_script.rb
require "bundler/setup"   # restricts loading to only Gemfile-listed gems
require "colorize"
require "httparty"

puts "Ready!".green

response = HTTParty.get("https://api.github.com")
puts response.code

Gem groups (e.g., test-only dependencies)

# Gemfile
gem "rails"

group :test do
  gem "rspec"
  gem "factory_bot"
end
bundle install --without test   # skip the :test group, e.g. in production

How It Actually Works

require and require_relative aren't preprocessor includes — each call asks Kernel#require to check $LOADED_FEATURES (an array of already-loaded absolute paths); if the file isn't there, Ruby reads it, compiles it to YARV bytecode, evaluates the whole file top to bottom (which is how class/def/constant definitions actually take effect), then records the path so re-requiring the same file is a no-op. RubyGems hooks into this by adding each installed gem's lib/ directory to $LOAD_PATH so require "gem_name" can find it. Bundler goes one step further: Gemfile.lock pins exact resolved versions of every gem and its transitive dependencies, and bundle exec (or Bundler.require in bundle/setup) rewrites $LOAD_PATH at runtime to point only at those locked versions — which is why running a script without bundle exec can silently load a different, system-installed version of a gem than the one your Gemfile.lock pinned.

Cheat sheet

Task Command
Install a gem globally gem install name
Install project dependencies bundle install
Run a script with locked versions bundle exec ruby script.rb
Add a gem Add a line to Gemfile, then bundle install
Pessimistic version constraint gem "name", "~> 1.2"
Group gems (e.g. test-only) group :test do ... end

🔀 See this in another language

Exercise

Create a Gemfile requiring the json gem (part of the standard library, but a good practice example) with a ~> version constraint. Run bundle install to generate Gemfile.lock. Then write a script that uses require "json" to parse a JSON string like '{"name": "Ada", "age": 30}' into a Ruby hash and print each key/value pair.