Second article in the series about building Ruby gems. In the previous article we talked about breaking the mental barrier. Now let’s open the box and see what’s inside.
One command and you have a gem
Seriously, open your terminal and type:
bundle gem my_gem
Bundler will ask you a couple of questions (testing framework, linter, license) and in seconds you’ll have a complete, functional structure. A real gem. Empty, yes, but a gem.
When I created Calendlyr, I did exactly this. Ran bundle gem and got to work. No manual configuration, no 45-minute tutorials. One command.
The structure it generates
This is what you get after running bundle gem my_gem:
my_gem/
├── lib/
│ ├── my_gem.rb # The entry point
│ └── my_gem/
│ └── version.rb # The gem version
├── test/ # Your tests
├── bin/
│ ├── console # IRB with your gem preloaded
│ └── setup # Setup script
├── my_gem.gemspec # The gem's config file
├── Gemfile # Development dependencies
├── Rakefile # Automated tasks
├── LICENSE.txt
└── README.md
It looks like a lot, but you’ll only touch most of these files once. The truly important ones are three: the gemspec, the entry point, and the lib/ folder. Everything else is scaffolding.
The .gemspec: your gem’s spec sheet
The .gemspec file is your gem’s config file. It tells RubyGems who you are, what your gem does, and what it needs to run.
Gem::Specification.new do |spec|
spec.name = "my_gem"
spec.version = MyGem::VERSION
spec.authors = ["your_name"]
spec.summary = "What your gem does in one line"
spec.files = `git ls-files -z`.split("\x0")
spec.require_paths = ["lib"]
spec.add_development_dependency "minitest", "~> 5.25"
end
The key lines:
- name and version: identify your gem on RubyGems.
- files: which files are included in the package. The
git ls-filestrick is elegant — if it’s in git, it goes in the gem. - require_paths: tells Ruby where to find your code. Always
["lib"]. - adddevelopmentdependency: dependencies you only need for development, not for using the gem.
Notice something: there’s no add_dependency (without the development). That would mean your gem needs another gem to work in production. You can have them, sure, but if you can avoid it, better. Fewer dependencies, fewer problems.
In Calendlyr I have zero production dependencies. None. Just net/http, json, and uri, which ship with Ruby. This was a conscious decision from day one, and we’ll talk more about it later.
The entry point: lib/my_gem.rb
This file is the front door. When someone does require "my_gem", Ruby loads this file. Everything starts here.
In its simplest form:
require "my_gem/version"
module MyGem
# Your code here
end
As your gem grows, this file becomes the index that connects everything. In Calendlyr, for example, I use autoload to load each part only when it’s needed:
module Calendlyr
autoload :Client, "calendlyr/client"
autoload :Object, "calendlyr/object"
autoload :Resource, "calendlyr/resource"
# Resources
autoload :EventsResource, "calendlyr/resources/events"
autoload :UsersResource, "calendlyr/resources/users"
# ...
end
autoload is a lazy (in the good way) way to load code. It doesn’t load the file until someone actually uses that class. For a gem, this is ideal: you don’t penalize the application’s boot time loading things that might never be used.
The lib/ folder: where your code lives
All your code goes inside lib/my_gem/. The convention is simple:
lib/my_gem.rb→ entry pointlib/my_gem/→ everything else
The internal structure is up to you. There are no strict rules. In Calendlyr I organized it like this:
lib/calendlyr/
├── client.rb # The HTTP client
├── resource.rb # Base class for resources
├── object.rb # JSON response wrapper
├── collection.rb # Pagination
├── configuration.rb # Global configuration
├── error.rb # Error handling
├── version.rb # Gem version
├── resources/ # One file per API resource
│ ├── events.rb
│ ├── users.rb
│ └── ...
└── objects/ # Typed response objects
├── event.rb
├── user.rb
└── ...
Does it look organized? Yes. Was it like this from day one? No. The first version had half these files. The structure grew as the gem needed it. Don’t try to design the perfect architecture from minute zero. Start simple and refactor when it hurts.
The Gemfile: less than you think
A gem’s Gemfile is surprisingly simple:
source "https://rubygems.org"
gemspec
Two lines. The magic is in gemspec: it tells Bundler to read the dependencies from your .gemspec. No need to duplicate anything. The development dependencies you declare in the gemspec will be automatically available.
The Rakefile: basic automation
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end
task default: :test
bundler/gem_tasks gives you rake build, rake install, and rake release for free. With that you can build, install locally, and publish your gem. All automated.
Rake::TestTask sets up the rake test command. And task default: :test means that typing just rake runs your tests. This also fits the simplicity philosophy: Minitest ships with Ruby, no extra gems needed.
bin/console: your sandbox
This is one of my favorite files and I think it’s underrated. bin/console opens an IRB session with your gem already loaded:
#!/usr/bin/env ruby
require "bundler/setup"
require "my_gem"
require "irb"
IRB.start(__FILE__)
Run bin/console and you can try your gem in real time. No setting up an application, no writing temporary scripts. It’s perfect for development.
In Calendlyr I went one step further and initialized the client directly:
client = Calendlyr::Client.new(token: ENV["CALENDLY_TOKEN"])
So I drop into the console and already have a client ready to play with.
The story behind the name
A small personal aside. When I needed to integrate Calendly, there was already a gem called calendly on RubyGems. The problem was it used OAuth, and my project needed Personal Access Token authentication to access all the subaccounts under the main team account — not a user-by-user authentication flow. That gem didn’t work for my use case.
So I decided to build my own. For a while both gems coexisted — I even linked to the other one in my README in case someone needed OAuth. I needed a different name, so I added the R. For Ruby. Calendly + R = Calendlyr.
Today that other gem is archived. Calendlyr is the one that remains. Sometimes the simple version outlives the complex one. There’s a lesson in that.
What actually matters
From everything we’ve covered, take this with you:
bundle gemdoes the heavy lifting — Don’t configure things by hand if you can avoid it.- The gemspec is your spec sheet — Fill it out properly and move on.
lib/is where your code lives — Entry point + organized folder.- The structure grows with you — Don’t try to design everything on day one.
A gem is just a folder with a specific structure and a .gemspec file. You’ve got that now. Time to fill it with code that does something useful.
In the next article we get to the good stuff: how to build an API wrapper from scratch. I’ll walk you through the Client → Resource → Object pattern I used in Calendlyr, using only net/http and zero dependencies. It’s easier than you think.